feat: 新增 automation 自动化模块和配置管理
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
# SaleShow 环境变量配置示例
|
||||||
|
# 复制此文件为 .env 并填写实际值
|
||||||
|
|
||||||
|
# Flask 配置
|
||||||
|
FLASK_DEBUG=False
|
||||||
|
PORT=5000
|
||||||
|
|
||||||
|
# Docker 端口映射(docker-compose 使用)
|
||||||
|
APP_PORT=5000
|
||||||
|
|
||||||
|
# secsion.com 登录凭据
|
||||||
|
# 也可通过 Web UI 设置页面配置(优先级更高)
|
||||||
|
SECSION_USERNAME=
|
||||||
|
SECSION_PASSWORD=
|
||||||
|
|
||||||
|
# 店铺 ID(留空则导出所有店铺数据)
|
||||||
|
SECSION_SHOP_ID=
|
||||||
|
|
||||||
|
# 定时任务配置
|
||||||
|
SCHEDULER_ENABLED=true
|
||||||
|
SCHEDULER_HOUR=1
|
||||||
|
SCHEDULER_MINUTE=0
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
"""
|
||||||
|
自动化模块 - 从 secsion.com 自动下载报表并导入 SaleShow
|
||||||
|
"""
|
||||||
|
from .secsion import SecsionDownloader
|
||||||
|
from .uploader import import_excel_file
|
||||||
|
from .scheduler import init_scheduler
|
||||||
|
|
||||||
|
__all__ = ['SecsionDownloader', 'import_excel_file', 'init_scheduler']
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
"""
|
||||||
|
定时调度模块
|
||||||
|
使用 APScheduler 实现每日自动下载前一天的销售数据
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
scheduler = None
|
||||||
|
|
||||||
|
|
||||||
|
def auto_download_job():
|
||||||
|
"""
|
||||||
|
定时任务:自动下载前一天的销售数据
|
||||||
|
每日凌晨 1 点执行
|
||||||
|
"""
|
||||||
|
logger.info("========== 定时任务触发:自动下载前一天数据 ==========")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from config import Config
|
||||||
|
from automation.secsion import SecsionDownloader
|
||||||
|
from automation.uploader import import_excel_file, cleanup_download
|
||||||
|
|
||||||
|
# 获取凭据
|
||||||
|
creds = Config.get_secsion_credentials()
|
||||||
|
if not creds:
|
||||||
|
logger.error("未配置 secsion.com 登录凭据,跳过自动下载")
|
||||||
|
return
|
||||||
|
|
||||||
|
username, password = creds
|
||||||
|
|
||||||
|
# 计算前一天日期
|
||||||
|
yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
|
||||||
|
logger.info(f"下载日期: {yesterday}")
|
||||||
|
|
||||||
|
# 获取店铺 ID
|
||||||
|
shop_id = Config.get_shop_id()
|
||||||
|
|
||||||
|
# 执行下载
|
||||||
|
downloader = SecsionDownloader(username, password, download_dir='downloads', shop_id=shop_id)
|
||||||
|
file_path = asyncio.run(downloader.download_report(yesterday, yesterday))
|
||||||
|
|
||||||
|
if not file_path:
|
||||||
|
logger.error("自动下载失败:未获取到文件")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 导入到 SaleShow
|
||||||
|
imported_name = import_excel_file(file_path, upload_dir='uploads')
|
||||||
|
if imported_name:
|
||||||
|
logger.info(f"自动导入成功: {imported_name}")
|
||||||
|
else:
|
||||||
|
logger.error("自动导入失败")
|
||||||
|
|
||||||
|
# 清理下载的临时文件
|
||||||
|
cleanup_download(file_path)
|
||||||
|
|
||||||
|
logger.info("========== 定时任务完成 ==========")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"定时任务执行异常: {e}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
def init_scheduler(app=None):
|
||||||
|
"""
|
||||||
|
初始化定时调度器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: Flask app 实例(可选,用于获取配置)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BackgroundScheduler: 调度器实例
|
||||||
|
"""
|
||||||
|
global scheduler
|
||||||
|
|
||||||
|
try:
|
||||||
|
from config import Config
|
||||||
|
schedule_config = Config.get_schedule_config()
|
||||||
|
except Exception:
|
||||||
|
schedule_config = {'enabled': True, 'hour': 1, 'minute': 0}
|
||||||
|
|
||||||
|
if not schedule_config.get('enabled', True):
|
||||||
|
logger.info("定时任务已禁用")
|
||||||
|
return None
|
||||||
|
|
||||||
|
hour = schedule_config.get('hour', 1)
|
||||||
|
minute = schedule_config.get('minute', 0)
|
||||||
|
|
||||||
|
scheduler = BackgroundScheduler()
|
||||||
|
scheduler.add_job(
|
||||||
|
func=auto_download_job,
|
||||||
|
trigger=CronTrigger(hour=hour, minute=minute),
|
||||||
|
id='daily_download',
|
||||||
|
name='每日自动下载销售数据',
|
||||||
|
replace_existing=True,
|
||||||
|
misfire_grace_time=3600 # 允许 1 小时的延迟执行
|
||||||
|
)
|
||||||
|
scheduler.start()
|
||||||
|
|
||||||
|
logger.info(f"定时任务已启动:每日 {hour:02d}:{minute:02d} 自动下载前一天数据")
|
||||||
|
return scheduler
|
||||||
|
|
||||||
|
|
||||||
|
def get_scheduler_status():
|
||||||
|
"""获取调度器状态"""
|
||||||
|
if scheduler is None:
|
||||||
|
return {'running': False, 'jobs': []}
|
||||||
|
|
||||||
|
jobs = []
|
||||||
|
for job in scheduler.get_jobs():
|
||||||
|
jobs.append({
|
||||||
|
'id': job.id,
|
||||||
|
'name': job.name,
|
||||||
|
'next_run': job.next_run_time.strftime('%Y-%m-%d %H:%M:%S') if job.next_run_time else None
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'running': scheduler.running,
|
||||||
|
'jobs': jobs
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def shutdown_scheduler():
|
||||||
|
"""关闭调度器"""
|
||||||
|
global scheduler
|
||||||
|
if scheduler and scheduler.running:
|
||||||
|
scheduler.shutdown(wait=False)
|
||||||
|
logger.info("定时任务已关闭")
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
"""
|
||||||
|
secsion.com 自动化下载模块
|
||||||
|
使用 Playwright 登录 secsion.com,导出销售报表
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import argparse
|
||||||
|
from datetime import datetime
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SecsionDownloader:
|
||||||
|
"""从 secsion.com 自动下载销售报表"""
|
||||||
|
|
||||||
|
LOGIN_URL = "https://secsion.com:8000/login?redirect=%252Fhomepage"
|
||||||
|
STATS_URL = "https://secsion.com:8000/commodityStatistics"
|
||||||
|
|
||||||
|
def __init__(self, username, password, download_dir=None, shop_id=None):
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.shop_id = shop_id or ''
|
||||||
|
self.download_dir = download_dir or os.path.join(os.getcwd(), "downloads")
|
||||||
|
os.makedirs(self.download_dir, exist_ok=True)
|
||||||
|
|
||||||
|
async def download_report(self, start_date, end_date):
|
||||||
|
"""
|
||||||
|
下载指定日期范围的销售报表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start_date: 开始日期 (YYYY-MM-DD)
|
||||||
|
end_date: 结束日期 (YYYY-MM-DD)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 下载文件的本地路径,失败返回 None
|
||||||
|
"""
|
||||||
|
logger.info(f"开始下载报表: {start_date} ~ {end_date}")
|
||||||
|
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch(headless=True)
|
||||||
|
context = await browser.new_context(
|
||||||
|
ignore_https_errors=True,
|
||||||
|
viewport={'width': 1280, 'height': 800}
|
||||||
|
)
|
||||||
|
page = await context.new_page()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self._login(page)
|
||||||
|
file_path = await self._export_report(page, start_date, end_date)
|
||||||
|
logger.info(f"报表下载完成: {file_path}")
|
||||||
|
return file_path
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"下载报表失败: {e}")
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
async def _login(self, page):
|
||||||
|
"""登录 secsion.com"""
|
||||||
|
logger.info(f"打开登录页面: {self.LOGIN_URL}")
|
||||||
|
await page.goto(self.LOGIN_URL)
|
||||||
|
|
||||||
|
# 选择角色 "店铺"
|
||||||
|
logger.info("选择角色: 店铺")
|
||||||
|
try:
|
||||||
|
await page.get_by_text("店铺", exact=True).click()
|
||||||
|
except Exception:
|
||||||
|
await page.click("text=店铺")
|
||||||
|
|
||||||
|
# 输入账号密码
|
||||||
|
logger.info(f"输入账号: {self.username}")
|
||||||
|
await page.get_by_placeholder("请输入用户名").fill(self.username)
|
||||||
|
await page.get_by_placeholder("请输入密码").fill(self.password)
|
||||||
|
|
||||||
|
# 勾选记住密码
|
||||||
|
if await page.get_by_text("记住密码").is_visible():
|
||||||
|
await page.get_by_text("记住密码").click()
|
||||||
|
|
||||||
|
# 点击登录
|
||||||
|
logger.info("点击登录按钮")
|
||||||
|
try:
|
||||||
|
await page.click("button:has-text('登录')", timeout=5000)
|
||||||
|
except Exception:
|
||||||
|
await page.click("button[type='submit']")
|
||||||
|
|
||||||
|
# 等待跳转
|
||||||
|
logger.info("等待登录跳转...")
|
||||||
|
await page.wait_for_url("**/homePage", timeout=20000)
|
||||||
|
logger.info("登录成功")
|
||||||
|
|
||||||
|
async def _export_report(self, page, start_date, end_date):
|
||||||
|
"""访问统计页面并导出报表"""
|
||||||
|
logger.info(f"访问统计页面: {self.STATS_URL}")
|
||||||
|
await page.goto(self.STATS_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
|
||||||
|
export_btn = page.get_by_role("button", name="导出报表")
|
||||||
|
await export_btn.wait_for(state="visible", timeout=20000)
|
||||||
|
|
||||||
|
logger.info(f"设置查询日期范围: {start_date} ~ {end_date}")
|
||||||
|
|
||||||
|
start_input = page.get_by_role("textbox", name="请选择日期").nth(0)
|
||||||
|
end_input = page.get_by_role("textbox", name="请选择日期").nth(1)
|
||||||
|
|
||||||
|
# 设置开始日期(内部已处理 Enter 确认 + Escape 关闭)
|
||||||
|
await self._set_date(page, start_input, start_date)
|
||||||
|
await page.wait_for_timeout(500)
|
||||||
|
|
||||||
|
# 设置结束日期
|
||||||
|
await self._set_date(page, end_input, end_date)
|
||||||
|
await page.wait_for_timeout(500)
|
||||||
|
|
||||||
|
# 验证日期设置结果
|
||||||
|
start_val = await start_input.input_value()
|
||||||
|
end_val = await end_input.input_value()
|
||||||
|
logger.info(f"日期设置结果: 开始={start_val}, 结束={end_val}")
|
||||||
|
|
||||||
|
# 等待数据请求完成
|
||||||
|
logger.info("等待数据请求完成...")
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
|
# 如果配置了 shop_id,拦截导出请求注入 shop_id
|
||||||
|
if self.shop_id:
|
||||||
|
import json
|
||||||
|
|
||||||
|
async def inject_shop_id(route):
|
||||||
|
request = route.request
|
||||||
|
body = json.loads(request.post_data)
|
||||||
|
body['shop_id'] = self.shop_id
|
||||||
|
logger.info(f"注入 shop_id: {self.shop_id}")
|
||||||
|
await route.continue_(post_data=json.dumps(body))
|
||||||
|
|
||||||
|
await page.route('**/api/bill/export', inject_shop_id)
|
||||||
|
logger.info(f"已设置 shop_id 拦截: {self.shop_id}")
|
||||||
|
|
||||||
|
# 点击导出报表并捕获下载
|
||||||
|
logger.info("点击导出报表...")
|
||||||
|
async with page.expect_download(timeout=60000) as download_info:
|
||||||
|
await export_btn.click()
|
||||||
|
|
||||||
|
download = await download_info.value
|
||||||
|
filename = download.suggested_filename
|
||||||
|
save_path = os.path.join(self.download_dir, filename)
|
||||||
|
await download.save_as(save_path)
|
||||||
|
logger.info(f"报表已保存至: {save_path}")
|
||||||
|
return save_path
|
||||||
|
|
||||||
|
async def _set_date(self, page, input_box, date_str):
|
||||||
|
"""
|
||||||
|
设置 TDesign 日期选择器的值
|
||||||
|
|
||||||
|
TDesign 的 needconfirm="true" 模式要求:
|
||||||
|
1. 点击输入框打开日历
|
||||||
|
2. 点击日期格子选择日期
|
||||||
|
3. 在输入框上按 Enter 确认(关键!不确认则关闭时回滚)
|
||||||
|
4. Escape 关闭日历
|
||||||
|
"""
|
||||||
|
for attempt in range(3):
|
||||||
|
logger.info(f"设置日期: {date_str} (第 {attempt + 1} 次尝试)")
|
||||||
|
|
||||||
|
# 1. 点击输入框打开日历
|
||||||
|
await input_box.click()
|
||||||
|
await page.wait_for_timeout(500)
|
||||||
|
|
||||||
|
# 2. 点击目标日期格子
|
||||||
|
target_day = str(int(date_str.split("-")[2]))
|
||||||
|
day_cells = page.get_by_role("cell", name=target_day)
|
||||||
|
cell_count = await day_cells.count()
|
||||||
|
|
||||||
|
if cell_count > 0:
|
||||||
|
await day_cells.first.click()
|
||||||
|
await page.wait_for_timeout(500)
|
||||||
|
else:
|
||||||
|
logger.warning(f"未找到日期格子: {target_day}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 3. Enter 确认(needconfirm="true" 必须显式确认)
|
||||||
|
await input_box.press("Enter")
|
||||||
|
await page.wait_for_timeout(500)
|
||||||
|
|
||||||
|
# 4. Escape 关闭日历
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
await page.wait_for_timeout(500)
|
||||||
|
|
||||||
|
# 5. 验证
|
||||||
|
val = await input_box.input_value()
|
||||||
|
if date_str in val:
|
||||||
|
logger.info(f"日期设置成功: {val}")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.warning(f"日期设置验证失败: 期望包含 '{date_str}', 实际 '{val}'")
|
||||||
|
|
||||||
|
logger.error(f"日期设置失败(3次尝试后): {date_str}")
|
||||||
|
|
||||||
|
|
||||||
|
async def download_report(start_date, end_date, username=None, password=None, download_dir=None, shop_id=None):
|
||||||
|
"""
|
||||||
|
便捷函数:下载指定日期范围的报表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start_date: 开始日期 (YYYY-MM-DD)
|
||||||
|
end_date: 结束日期 (YYYY-MM-DD)
|
||||||
|
username: secsion.com 用户名(可选,优先使用 config)
|
||||||
|
password: secsion.com 密码(可选,优先使用 config)
|
||||||
|
download_dir: 下载目录(可选)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 下载文件路径,失败返回 None
|
||||||
|
"""
|
||||||
|
if not username or not password:
|
||||||
|
from config import Config
|
||||||
|
creds = Config.get_secsion_credentials()
|
||||||
|
if not creds:
|
||||||
|
logger.error("未配置 secsion.com 登录凭据")
|
||||||
|
return None
|
||||||
|
username, password = creds
|
||||||
|
|
||||||
|
if not shop_id:
|
||||||
|
try:
|
||||||
|
from config import Config
|
||||||
|
shop_id = Config.get_shop_id()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
downloader = SecsionDownloader(username, password, download_dir, shop_id)
|
||||||
|
return await downloader.download_report(start_date, end_date)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.StreamHandler(),
|
||||||
|
logging.FileHandler('automation.log', encoding='utf-8')
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='secsion.com 报表自动下载工具')
|
||||||
|
parser.add_argument('--start', type=str, help='开始日期 (YYYY-MM-DD)', default=datetime.now().strftime('%Y-%m-%d'))
|
||||||
|
parser.add_argument('--end', type=str, help='结束日期 (YYYY-MM-DD)')
|
||||||
|
parser.add_argument('--username', type=str, help='secsion.com 用户名')
|
||||||
|
parser.add_argument('--password', type=str, help='secsion.com 密码')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
end_date = args.end or args.start
|
||||||
|
|
||||||
|
result = asyncio.run(download_report(
|
||||||
|
start_date=args.start,
|
||||||
|
end_date=end_date,
|
||||||
|
username=args.username,
|
||||||
|
password=args.password
|
||||||
|
))
|
||||||
|
|
||||||
|
if result:
|
||||||
|
print(f"下载成功: {result}")
|
||||||
|
else:
|
||||||
|
print("下载失败")
|
||||||
|
exit(1)
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""
|
||||||
|
本地文件导入模块
|
||||||
|
将下载的 Excel 文件直接导入 SaleShow 的 uploads 目录
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def import_excel_file(source_path, upload_dir='uploads'):
|
||||||
|
"""
|
||||||
|
将 Excel 文件导入 SaleShow 的 uploads 目录
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_path: 源文件路径
|
||||||
|
upload_dir: SaleShow 的上传目录路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 导入后的文件名(带时间戳前缀),失败返回 None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(source_path):
|
||||||
|
logger.error(f"源文件不存在: {source_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 确保上传目录存在
|
||||||
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 生成带时间戳的文件名(与 SaleShow 手动上传的命名规则一致)
|
||||||
|
original_name = os.path.basename(source_path)
|
||||||
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_')
|
||||||
|
new_filename = timestamp + original_name
|
||||||
|
dest_path = os.path.join(upload_dir, new_filename)
|
||||||
|
|
||||||
|
# 复制文件
|
||||||
|
shutil.copy2(source_path, dest_path)
|
||||||
|
logger.info(f"文件已导入: {source_path} -> {dest_path}")
|
||||||
|
|
||||||
|
return new_filename
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"导入文件失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_download(filepath):
|
||||||
|
"""清理下载的临时文件"""
|
||||||
|
try:
|
||||||
|
if filepath and os.path.exists(filepath):
|
||||||
|
os.remove(filepath)
|
||||||
|
logger.info(f"已清理临时文件: {filepath}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"清理临时文件失败: {e}")
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
"""
|
||||||
|
配置管理模块
|
||||||
|
优先级:Web UI 设置 (data/config.json) > 环境变量 > 默认值
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data', 'config.json')
|
||||||
|
|
||||||
|
# 默认配置
|
||||||
|
DEFAULT_CONFIG = {
|
||||||
|
'secsion': {
|
||||||
|
'username': '',
|
||||||
|
'password': ''
|
||||||
|
},
|
||||||
|
'scheduler': {
|
||||||
|
'enabled': True,
|
||||||
|
'hour': 1,
|
||||||
|
'minute': 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_config_dir():
|
||||||
|
"""确保配置目录存在"""
|
||||||
|
os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_config():
|
||||||
|
"""从 config.json 加载配置"""
|
||||||
|
try:
|
||||||
|
if os.path.exists(CONFIG_FILE):
|
||||||
|
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"读取配置文件失败: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _save_config(config):
|
||||||
|
"""保存配置到 config.json"""
|
||||||
|
_ensure_config_dir()
|
||||||
|
try:
|
||||||
|
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||||
|
logger.info("配置已保存")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"保存配置文件失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""配置管理器"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_secsion_credentials():
|
||||||
|
"""
|
||||||
|
获取 secsion.com 登录凭据
|
||||||
|
|
||||||
|
优先级:config.json > 环境变量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (username, password) 或 None
|
||||||
|
"""
|
||||||
|
# 1. 从 config.json 读取
|
||||||
|
config = _load_config()
|
||||||
|
secsion = config.get('secsion', {})
|
||||||
|
username = secsion.get('username', '').strip()
|
||||||
|
password = secsion.get('password', '').strip()
|
||||||
|
|
||||||
|
if username and password:
|
||||||
|
return (username, password)
|
||||||
|
|
||||||
|
# 2. 从环境变量读取
|
||||||
|
env_username = os.environ.get('SECSION_USERNAME', '').strip()
|
||||||
|
env_password = os.environ.get('SECSION_PASSWORD', '').strip()
|
||||||
|
|
||||||
|
if env_username and env_password:
|
||||||
|
return (env_username, env_password)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_secsion_credentials(username, password):
|
||||||
|
"""保存 secsion.com 登录凭据"""
|
||||||
|
config = _load_config()
|
||||||
|
config.setdefault('secsion', {})
|
||||||
|
config['secsion']['username'] = username
|
||||||
|
config['secsion']['password'] = password
|
||||||
|
return _save_config(config)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_schedule_config():
|
||||||
|
"""
|
||||||
|
获取定时任务配置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {'enabled': bool, 'hour': int, 'minute': int}
|
||||||
|
"""
|
||||||
|
config = _load_config()
|
||||||
|
schedule = config.get('scheduler', {})
|
||||||
|
|
||||||
|
# 回退到环境变量
|
||||||
|
env_enabled = os.environ.get('SCHEDULER_ENABLED', '').lower()
|
||||||
|
env_hour = os.environ.get('SCHEDULER_HOUR', '')
|
||||||
|
env_minute = os.environ.get('SCHEDULER_MINUTE', '')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'enabled': schedule.get('enabled', env_enabled != 'false' if env_enabled else True),
|
||||||
|
'hour': schedule.get('hour', int(env_hour) if env_hour else 1),
|
||||||
|
'minute': schedule.get('minute', int(env_minute) if env_minute else 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_schedule_config(enabled=True, hour=1, minute=0):
|
||||||
|
"""保存定时任务配置"""
|
||||||
|
config = _load_config()
|
||||||
|
config.setdefault('scheduler', {})
|
||||||
|
config['scheduler']['enabled'] = enabled
|
||||||
|
config['scheduler']['hour'] = hour
|
||||||
|
config['scheduler']['minute'] = minute
|
||||||
|
return _save_config(config)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_shop_id():
|
||||||
|
"""获取店铺 ID"""
|
||||||
|
config = _load_config()
|
||||||
|
secsion = config.get('secsion', {})
|
||||||
|
shop_id = secsion.get('shop_id', '').strip()
|
||||||
|
if shop_id:
|
||||||
|
return shop_id
|
||||||
|
return os.environ.get('SECSION_SHOP_ID', '').strip() or None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_shop_id(shop_id):
|
||||||
|
"""保存店铺 ID"""
|
||||||
|
config = _load_config()
|
||||||
|
config.setdefault('secsion', {})
|
||||||
|
config['secsion']['shop_id'] = shop_id
|
||||||
|
return _save_config(config)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all_config():
|
||||||
|
"""获取所有配置(密码脱敏)"""
|
||||||
|
config = _load_config()
|
||||||
|
secsion = config.get('secsion', {})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'secsion': {
|
||||||
|
'username': secsion.get('username', ''),
|
||||||
|
'password': '******' if secsion.get('password') else '',
|
||||||
|
'shop_id': secsion.get('shop_id', '')
|
||||||
|
},
|
||||||
|
'scheduler': config.get('scheduler', DEFAULT_CONFIG['scheduler'])
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user