703 lines
23 KiB
Python
703 lines
23 KiB
Python
# -*- coding:utf-8 -*-
|
||
"""
|
||
@File : meian_db
|
||
@Author : xuxingchen
|
||
@Version : 1.0
|
||
@Contact : xuxingchen@sinochem.com
|
||
@Desc : meian database crud
|
||
"""
|
||
import csv
|
||
import json
|
||
import os.path
|
||
import sqlite3
|
||
import time
|
||
import traceback
|
||
|
||
from logger import Logger, new_dc
|
||
from datetime import datetime
|
||
from devices.meian_model import HeartBeat, Register, PushRtAccessRecord
|
||
from devices.common_model import UserInfo
|
||
from utils import datetime_to_timestamp
|
||
from config import ENV_TYPE, DEBUG
|
||
|
||
|
||
class SQLiteDatabaseEngine:
|
||
def __init__(self, db_path: str = "demo.db") -> None:
|
||
self.sqlite3 = sqlite3
|
||
self.db_path = db_path
|
||
self.connection = None
|
||
self.cursor = None
|
||
self.connect()
|
||
|
||
def __del__(self):
|
||
# self.disconnect()
|
||
pass
|
||
|
||
def connect(self):
|
||
"""连接SQLite 数据库(如果数据库不存在则会自动创建)"""
|
||
self.connection = self.sqlite3.connect(self.db_path)
|
||
self.cursor = self.connection.cursor()
|
||
Logger.init(new_dc(f"🔗 SQLite - {self.db_path} has connect successfully! 🔗", "[1;32m"))
|
||
|
||
def disconnect(self):
|
||
try:
|
||
self.cursor.close()
|
||
self.connection.close()
|
||
except Exception as e:
|
||
if type(e).__name__ != "ProgrammingError":
|
||
Logger.error(f"{type(e).__name__}, {e}")
|
||
Logger.info(new_dc(f"🔌 Disconnect from SQLite - {self.db_path}! 🔌", "[1m"))
|
||
|
||
def exist(self, table_name):
|
||
self.cursor.execute(
|
||
f"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
|
||
(table_name,),
|
||
)
|
||
result = self.cursor.fetchone()
|
||
if result:
|
||
return True
|
||
else:
|
||
return False
|
||
|
||
|
||
class BaseTable:
|
||
def __init__(self, connection=None, cursor=None):
|
||
self.connection = connection
|
||
self.cursor = cursor
|
||
|
||
def set(self, connection, cursor):
|
||
self.connection = connection
|
||
self.cursor = cursor
|
||
|
||
def execute(self, sql: str, params: tuple = ()):
|
||
self.cursor.execute(sql, params)
|
||
self.connection.commit()
|
||
|
||
def executemany(self, sql: str, params: list[tuple]):
|
||
self.cursor.executemany(sql, params)
|
||
self.connection.commit()
|
||
|
||
def query(self, sql: str, params: tuple = ()):
|
||
self.cursor.execute(sql, params)
|
||
|
||
|
||
class DeviceTable(BaseTable):
|
||
@staticmethod
|
||
def check(table_handler: BaseTable):
|
||
"""检测是否存在当前表,并根据devices.csv开始数据初始化"""
|
||
table_handler.query("SELECT name FROM sqlite_master WHERE type='table' AND name='device'")
|
||
if table_handler.cursor.fetchone() is None:
|
||
table_handler.execute(
|
||
f"""
|
||
CREATE TABLE device (
|
||
device_id TEXT,
|
||
project_code TEXT,
|
||
subscribe_topic TEXT,
|
||
publish_topic TEXT,
|
||
aiot_id TEXT NULL,
|
||
register_type INTEGER default 0,
|
||
PRIMARY KEY (device_id)
|
||
)
|
||
"""
|
||
)
|
||
init_config_path = os.path.join(os.path.dirname((os.path.abspath("__file__"))), "data",
|
||
"_devices.csv" if ENV_TYPE != 2 else "devices.csv")
|
||
if os.path.exists(init_config_path):
|
||
with open(init_config_path, newline='', encoding="utf8") as csvfile:
|
||
csvreader = csv.reader(csvfile)
|
||
head = next(csvreader)
|
||
data = []
|
||
if len(head) == 5:
|
||
for row in csvreader:
|
||
register_type = 0 if row[4] == '' else 1
|
||
data.append(tuple([i.strip() for i in row] + [register_type]))
|
||
|
||
table_handler.executemany(
|
||
f"""
|
||
INSERT INTO device
|
||
(project_code, device_id, subscribe_topic, publish_topic, aiot_id, register_type)
|
||
VALUES (?, ?, ?, ?, ?, ?)
|
||
ON CONFLICT (device_id) DO NOTHING
|
||
""",
|
||
data
|
||
)
|
||
|
||
@staticmethod
|
||
def get_topics(table_handler):
|
||
table_handler.query(
|
||
"""
|
||
SELECT (
|
||
SELECT json_group_array(subscribe_topic)
|
||
FROM (
|
||
SELECT DISTINCT subscribe_topic
|
||
FROM device
|
||
)
|
||
) AS subscribe_topics,
|
||
(
|
||
SELECT json_group_array(publish_topic)
|
||
FROM (
|
||
SELECT DISTINCT publish_topic
|
||
FROM device
|
||
)
|
||
) AS publish_topics;
|
||
"""
|
||
)
|
||
res = table_handler.cursor.fetchall()
|
||
if res:
|
||
return res[0]
|
||
else:
|
||
return None
|
||
|
||
@staticmethod
|
||
def check_device_id(table_handler, topic, device_id):
|
||
"""device in `publish_topic` and `device_id` column"""
|
||
table_handler.query(
|
||
f"""
|
||
SELECT DISTINCT device_id from device where publish_topic = '{topic}' and device_id = '{device_id}'
|
||
"""
|
||
)
|
||
if len(table_handler.cursor.fetchall()) > 0:
|
||
return True
|
||
else:
|
||
return False
|
||
|
||
@staticmethod
|
||
def get_device_topic(table_handler, device_id):
|
||
"""获取对应设备的订阅主题"""
|
||
table_handler.query(
|
||
f"""
|
||
SELECT subscribe_topic, publish_topic from device where device_id = '{device_id}'
|
||
"""
|
||
)
|
||
res = table_handler.cursor.fetchall()
|
||
if res:
|
||
return res[0]
|
||
else:
|
||
return None
|
||
|
||
@staticmethod
|
||
def get_device_register_type(table_handler, device_id):
|
||
table_handler.query(
|
||
f"""
|
||
SELECT register_type from device where device_id = '{device_id}'
|
||
"""
|
||
)
|
||
res = table_handler.cursor.fetchall()
|
||
if res and res[0][0] == 1:
|
||
return True
|
||
else:
|
||
return False
|
||
|
||
@staticmethod
|
||
def get_aiot_id(table_handler, device_id):
|
||
table_handler.query(
|
||
f"""
|
||
SELECT aiot_id from device where device_id = '{device_id}' and register_type = 1
|
||
"""
|
||
)
|
||
res = table_handler.cursor.fetchall()
|
||
if res:
|
||
return res[0][0]
|
||
else:
|
||
return None
|
||
|
||
@staticmethod
|
||
def get_device_id(table_handler, aiot_id):
|
||
table_handler.query(
|
||
f"""
|
||
SELECT device_id from device where aiot_id = '{aiot_id}' and register_type = 1
|
||
"""
|
||
)
|
||
res = table_handler.cursor.fetchall()
|
||
if res:
|
||
return res[0][0]
|
||
else:
|
||
return None
|
||
|
||
@staticmethod
|
||
def get_device_ids(table_handler):
|
||
"""获取所有设备的ID"""
|
||
table_handler.query(
|
||
f"""
|
||
SELECT device_id from device
|
||
"""
|
||
)
|
||
res = table_handler.cursor.fetchall()
|
||
if res:
|
||
return res
|
||
else:
|
||
return None
|
||
|
||
@staticmethod
|
||
def get_project_code(table_handler, device_id):
|
||
table_handler.query(
|
||
f"""
|
||
SELECT project_code from device where device_id = '{device_id}'
|
||
"""
|
||
)
|
||
res = table_handler.cursor.fetchall()
|
||
if res:
|
||
return res[0][0]
|
||
else:
|
||
return None
|
||
|
||
@staticmethod
|
||
def update_aiot_id(table_handler: BaseTable, device_id, aiot_id):
|
||
table_handler.execute(
|
||
f"""
|
||
UPDATE device SET aiot_id = '{aiot_id}', register_type = 1 WHERE device_id = '{device_id}'
|
||
"""
|
||
)
|
||
|
||
|
||
class GatewayTable(BaseTable):
|
||
@staticmethod
|
||
def check(table_handler: BaseTable):
|
||
"""检测是否存在当前表,并根据gateway.json开始数据初始化"""
|
||
table_handler.query("SELECT name FROM sqlite_master WHERE type='table' AND name='gateway'")
|
||
if table_handler.cursor.fetchone() is None:
|
||
table_handler.execute(
|
||
f"""
|
||
CREATE TABLE gateway (
|
||
gateway_id TEXT NULL,
|
||
project_code TEXT,
|
||
gateway_sct TEXT NULL,
|
||
register_type INTEGER default 0,
|
||
PRIMARY KEY (gateway_id)
|
||
)
|
||
"""
|
||
)
|
||
init_config_path = os.path.join(os.path.dirname(os.path.abspath("__file__")), "data",
|
||
"_gateways.json" if ENV_TYPE != 2 else "gateways.json")
|
||
if os.path.exists(init_config_path):
|
||
with open(init_config_path, "r", encoding="utf8") as f:
|
||
try:
|
||
gateway_config = json.load(f)
|
||
data = []
|
||
for project_code, gateway_info in gateway_config.items():
|
||
data.append((project_code, gateway_info[0], gateway_info[1], 1))
|
||
table_handler.executemany(
|
||
f"""
|
||
INSERT INTO gateway (project_code, gateway_id, gateway_sct, register_type) VALUES (?, ?, ?, ?)
|
||
ON CONFLICT (gateway_id) DO NOTHING
|
||
""",
|
||
data
|
||
)
|
||
except Exception as e:
|
||
Logger.error(f"{type(e).__name__}, {e}")
|
||
if DEBUG:
|
||
traceback.print_exc()
|
||
|
||
@staticmethod
|
||
def get_gateway(table_handler, project_code):
|
||
table_handler.query(
|
||
f"""
|
||
SELECT gateway_id, gateway_sct from gateway where project_code = '{project_code}'
|
||
"""
|
||
)
|
||
res = table_handler.cursor.fetchall()
|
||
if res:
|
||
return res[0]
|
||
else:
|
||
return None, None
|
||
|
||
@staticmethod
|
||
def get_registered_gateway(table_handler):
|
||
table_handler.query(
|
||
f"""
|
||
SELECT gateway_id, gateway_sct from gateway where register_type = 1
|
||
"""
|
||
)
|
||
res = table_handler.cursor.fetchall()
|
||
if res:
|
||
return res
|
||
else:
|
||
return None
|
||
|
||
@staticmethod
|
||
def get_project_code_list(table_handler):
|
||
table_handler.query(
|
||
f"""
|
||
SELECT DISTINCT project_code from gateway
|
||
"""
|
||
)
|
||
res = table_handler.cursor.fetchall()
|
||
if res:
|
||
return res
|
||
else:
|
||
return []
|
||
|
||
@staticmethod
|
||
def get_registered_sub_aiot_id(table_handler, gateway_id):
|
||
"""查询已注册网关下的所有子设备在aiot平台的设备id"""
|
||
table_handler.query(
|
||
f"""
|
||
SELECT json_group_array(aiot_id) FROM device WHERE project_code IN
|
||
(SELECT DISTINCT project_code FROM gateway
|
||
WHERE register_type = 1 and gateway_id = '{gateway_id}')
|
||
AND register_type = 1
|
||
"""
|
||
)
|
||
res = table_handler.cursor.fetchall()
|
||
if res:
|
||
return res[0]
|
||
else:
|
||
return None
|
||
|
||
|
||
class HeartBeatTable(BaseTable):
|
||
@staticmethod
|
||
def check(table_handler: BaseTable):
|
||
table_handler.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS heart_beat (
|
||
device_id TEXT,
|
||
factory_id TEXT,
|
||
last_heart_beat TEXT,
|
||
PRIMARY KEY (device_id)
|
||
)
|
||
"""
|
||
)
|
||
|
||
@staticmethod
|
||
def delete(table_handler: BaseTable, obj):
|
||
table_handler.execute("DELETE FROM heart_beat WHERE device_id=?", (obj.device_id,))
|
||
|
||
@staticmethod
|
||
def update(table_handler: BaseTable, obj: HeartBeat, topic: str):
|
||
if DeviceTable.check_device_id(table_handler, topic, obj.device_id):
|
||
time_stamp = str(int(time.time()))
|
||
table_handler.execute(
|
||
"""
|
||
INSERT INTO heart_beat (device_id, factory_id, last_heart_beat)
|
||
VALUES (?, ?, ?)
|
||
ON CONFLICT (device_id)
|
||
DO UPDATE SET
|
||
last_heart_beat=?
|
||
""",
|
||
(obj.device_id, obj.factory_id, time_stamp, time_stamp),
|
||
)
|
||
return True
|
||
else:
|
||
if DEBUG:
|
||
Logger.warn(
|
||
f"device_id - {obj.device_id} is invalid in {topic}, operation was not performed"
|
||
)
|
||
return False
|
||
|
||
@staticmethod
|
||
def get_last_time(table_handler: BaseTable, device_id):
|
||
table_handler.query(
|
||
"""
|
||
SELECT last_heart_beat
|
||
FROM heart_beat
|
||
WHERE device_id = ?
|
||
""",
|
||
(device_id,),
|
||
)
|
||
res = table_handler.cursor.fetchall()
|
||
if res:
|
||
return res[0][0]
|
||
else:
|
||
return None
|
||
|
||
|
||
class RegisterTable(BaseTable):
|
||
@staticmethod
|
||
def check(table_handler: BaseTable):
|
||
table_handler.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS register (
|
||
device_id TEXT,
|
||
factory_id TEXT,
|
||
device_type TEXT,
|
||
device_position_code TEXT,
|
||
device_position_desc TEXT,
|
||
last_register_timestamp TEXT,
|
||
PRIMARY KEY (device_id)
|
||
)
|
||
"""
|
||
)
|
||
|
||
def drop(self):
|
||
self.execute("drop table register")
|
||
|
||
def delete(self, obj):
|
||
self.execute(
|
||
"DELETE FROM register WHERE device_id=? and factory_id=?",
|
||
(
|
||
obj.device_id,
|
||
obj.factory_id,
|
||
),
|
||
)
|
||
|
||
def insert(self, obj, topic: str):
|
||
if DeviceTable.check_device_id(self, topic, obj.device_id):
|
||
time_stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
||
self.execute(
|
||
"""
|
||
INSERT INTO register
|
||
(device_id, factory_id, device_type, device_position_code, device_position_desc,
|
||
last_register_timestamp)
|
||
VALUES (?, ?, ?, ?, ?, ?)
|
||
""",
|
||
(
|
||
obj.device_id,
|
||
obj.factory_id,
|
||
obj.device_type,
|
||
obj.device_position_code,
|
||
obj.device_position_desc,
|
||
time_stamp,
|
||
),
|
||
)
|
||
return True
|
||
else:
|
||
if DEBUG:
|
||
Logger.warn(
|
||
f"device_id - {obj.device_id} is invalid in {topic}, operation was not performed"
|
||
)
|
||
return False
|
||
|
||
@staticmethod
|
||
def update(table_handler, obj: Register, topic: str):
|
||
if DeviceTable.check_device_id(table_handler, topic, obj.device_id):
|
||
time_stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
||
table_handler.execute(
|
||
"""
|
||
INSERT INTO register (device_id, factory_id, device_type, device_position_code, device_position_desc,
|
||
last_register_timestamp)
|
||
VALUES (?, ?, ?, ?, ?, ?)
|
||
ON CONFLICT (device_id)
|
||
DO UPDATE SET
|
||
device_type=?, device_position_code=?, device_position_desc=?, last_register_timestamp=?
|
||
""",
|
||
(
|
||
obj.device_id,
|
||
obj.factory_id,
|
||
obj.device_type,
|
||
obj.device_position_code,
|
||
obj.device_position_desc,
|
||
time_stamp,
|
||
obj.device_type,
|
||
obj.device_position_code,
|
||
obj.device_position_desc,
|
||
time_stamp,
|
||
),
|
||
)
|
||
return True
|
||
else:
|
||
if DEBUG:
|
||
Logger.warn(
|
||
f"device_id - {obj.device_id} is invalid in {topic}, operation was not performed"
|
||
)
|
||
return False
|
||
|
||
@staticmethod
|
||
def get_device_position_desc(table_handler: BaseTable, device_id: str):
|
||
"""根据device_id获取设备的空间描述信息"""
|
||
table_handler.query(
|
||
"""
|
||
SELECT device_position_desc
|
||
FROM register
|
||
WHERE device_id = ?
|
||
""",
|
||
(device_id,),
|
||
)
|
||
res = table_handler.cursor.fetchall()
|
||
if res:
|
||
return res[0][0]
|
||
else:
|
||
return None
|
||
|
||
|
||
class UserInfoTable(BaseTable):
|
||
@staticmethod
|
||
def check(table_handler: BaseTable):
|
||
table_handler.query("SELECT name FROM sqlite_master WHERE type='table' AND name='user_info'")
|
||
if table_handler.cursor.fetchone() is None:
|
||
table_handler.execute(
|
||
f"""
|
||
CREATE TABLE user_info (
|
||
user_id TEXT,
|
||
device_id TEXT,
|
||
name TEXT,
|
||
user_type INTEGER,
|
||
qrcode TEXT NULL,
|
||
face_url TEXT NULL,
|
||
create_timestamp TEXT,
|
||
update_timestamp TEXT,
|
||
PRIMARY KEY (user_id, device_id)
|
||
)
|
||
"""
|
||
)
|
||
table_handler.execute(
|
||
f"""
|
||
CREATE INDEX idx_user_info_qrcode ON user_info(qrcode);
|
||
"""
|
||
)
|
||
init_config_path = os.path.join(os.path.dirname(os.path.abspath("__file__")), "data",
|
||
"_users.csv" if ENV_TYPE != 2 else "users.csv")
|
||
if os.path.exists(init_config_path):
|
||
with open(init_config_path, newline='', encoding="utf8") as csvfile:
|
||
csvreader = csv.reader(csvfile)
|
||
head = next(csvreader)
|
||
data = []
|
||
if len(head) == 4:
|
||
for row in csvreader:
|
||
user_id = row[0].strip()
|
||
name = row[1].strip()
|
||
user_type = 0 if row[2].strip() == "业主" else 1
|
||
timestamp = str(int(time.time() * 1000))
|
||
device_id = row[3].strip()
|
||
data.append((user_id, name, user_type, timestamp, timestamp, device_id))
|
||
table_handler.executemany(
|
||
f"""
|
||
INSERT INTO user_info
|
||
(user_id, name, user_type, create_timestamp, update_timestamp, device_id)
|
||
VALUES (?, ?, ?, ?, ?, ?)
|
||
ON CONFLICT (user_id, device_id) DO NOTHING
|
||
""",
|
||
data
|
||
)
|
||
elif len(head) == 5:
|
||
for row in csvreader:
|
||
user_id = row[0].strip()
|
||
name = row[1].strip()
|
||
user_type = 0 if row[2].strip() == "业主" else 1
|
||
timestamp = str(int(time.time() * 1000))
|
||
device_id = row[3].strip()
|
||
face_url = row[4].strip()
|
||
data.append((user_id, name, user_type, timestamp, timestamp, device_id, face_url))
|
||
table_handler.executemany(
|
||
f"""
|
||
INSERT INTO user_info
|
||
(user_id, name, user_type, create_timestamp, update_timestamp, device_id, face_url)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||
ON CONFLICT (user_id, device_id) DO NOTHING
|
||
""",
|
||
data
|
||
)
|
||
|
||
@staticmethod
|
||
def delete_redundancy_1(table_handler, timestamp):
|
||
"""用于在每天清理无效的冗余访客数据"""
|
||
table_handler.execute(f"DELETE FROM user_info WHERE user_type=1 and update_timestamp < {timestamp}")
|
||
|
||
@staticmethod
|
||
def delete_redundancy_0(table_handler, timestamp):
|
||
"""用于在每天清理无效的冗余业主数据"""
|
||
table_handler.execute(f"DELETE FROM user_info WHERE user_type=0 "
|
||
f"and (face_url is NULL or face_url = '') and update_timestamp < {timestamp}")
|
||
|
||
@staticmethod
|
||
def update(table_handler, obj: UserInfo):
|
||
time_stamp = str(int(time.time() * 1000))
|
||
table_handler.execute(
|
||
"""
|
||
INSERT INTO user_info (user_id, name, user_type, create_timestamp, update_timestamp, device_id)
|
||
VALUES (?, ?, ?, ?, ?, ?)
|
||
ON CONFLICT (user_id, device_id)
|
||
DO UPDATE SET
|
||
name=?, update_timestamp=?
|
||
""",
|
||
(obj.user_id, obj.name, obj.user_type, time_stamp, time_stamp, obj.device_id,
|
||
obj.name, time_stamp),
|
||
)
|
||
|
||
@staticmethod
|
||
def select_all(table_handler):
|
||
table_handler.execute("SELECT * FROM user_info")
|
||
return table_handler.cursor.fetchall()
|
||
|
||
@staticmethod
|
||
def get_name(table_handler, user_id, device_id):
|
||
table_handler.query(
|
||
"""
|
||
SELECT name
|
||
FROM user_info
|
||
WHERE user_id = ? AND device_id = ?
|
||
""",
|
||
(user_id, device_id),
|
||
)
|
||
res = table_handler.cursor.fetchall()
|
||
if res:
|
||
return res[0][0]
|
||
else:
|
||
return None
|
||
|
||
@staticmethod
|
||
def get_user_by_qrcode(table_handler, qrcode, device_id):
|
||
table_handler.query(
|
||
"""
|
||
SELECT user_id, name
|
||
FROM user_info
|
||
WHERE qrcode = ? AND device_id = ?
|
||
""",
|
||
(qrcode, device_id),
|
||
)
|
||
res = table_handler.cursor.fetchall()
|
||
if res:
|
||
return res[0]
|
||
else:
|
||
return None
|
||
|
||
@staticmethod
|
||
def update_qrcode(table_handler, user_id, device_id, qrcode):
|
||
time_stamp = str(int(time.time() * 1000))
|
||
table_handler.execute(
|
||
f"""
|
||
UPDATE user_info SET qrcode = ?, update_timestamp = ? WHERE user_id = ? and device_id = ?
|
||
""",
|
||
(qrcode, time_stamp, user_id, device_id)
|
||
)
|
||
|
||
@staticmethod
|
||
def update_face_url(table_handler, user_id, device_id, face_url):
|
||
time_stamp = str(int(time.time() * 1000))
|
||
table_handler.execute(
|
||
f"""
|
||
UPDATE user_info SET face_url = ?, update_timestamp = ? WHERE user_id = ? and device_id = ?
|
||
""",
|
||
(face_url, time_stamp, user_id, device_id)
|
||
)
|
||
|
||
@staticmethod
|
||
def exists_face_url(table_handler, user_id, device_id):
|
||
"""判断人脸地址是否存在且不为空"""
|
||
table_handler.query(
|
||
f"""
|
||
SELECT face_url FROM user_info WHERE user_id = ? AND device_id = ?
|
||
AND face_url IS NOT NULL AND face_url != ''
|
||
""",
|
||
(user_id, device_id)
|
||
)
|
||
res = table_handler.cursor.fetchall()
|
||
if res and len(res[0]) > 0:
|
||
return True
|
||
else:
|
||
return False
|
||
|
||
|
||
class RecordTable(BaseTable):
|
||
@staticmethod
|
||
def check(table_handler: BaseTable):
|
||
table_handler.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS record (
|
||
user_id TEXT,
|
||
device_id TEXT,
|
||
record_datetime TEXT
|
||
)
|
||
"""
|
||
)
|
||
|
||
@staticmethod
|
||
def add(table_handler: BaseTable, obj: PushRtAccessRecord):
|
||
table_handler.execute(
|
||
"""
|
||
INSERT INTO record (user_id, device_id, record_datetime)
|
||
VALUES (?, ?, ?)
|
||
""",
|
||
(obj.user_id, obj.device_id, str(datetime_to_timestamp(obj.time))),
|
||
)
|