242 lines
8.4 KiB
Python
242 lines
8.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
小龙 sheet update: match phones via XXTEA encryption, fill H (UID), D (trial count), I (reg date), J (channel)
|
|
"""
|
|
import sys
|
|
sys.path.insert(0, '/root/.openclaw/workspace/scripts')
|
|
from phone_encrypt import encrypt_phone
|
|
import psycopg2
|
|
import json
|
|
import requests
|
|
import time
|
|
|
|
FEISHU_TOKEN = "t-g10464c0UK5L67JVXSDDT3EWM4DPLSDY5C7R7NS6"
|
|
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
|
|
SHEET_ID = "qJF4I"
|
|
|
|
# PostgreSQL connection
|
|
PG_CONFIG = {
|
|
"host": "bj-postgres-16pob4sg.sql.tencentcdb.com",
|
|
"port": 28591,
|
|
"user": "ai_member",
|
|
"password": "LdfjdjL83h3h3^$&**YGG*",
|
|
"database": "vala_bi",
|
|
}
|
|
|
|
def get_sheet_data():
|
|
"""Read all data from the sheet"""
|
|
url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values/{SHEET_ID}!A3:J2502?valueRenderOption=ToString"
|
|
headers = {"Authorization": f"Bearer {FEISHU_TOKEN}"}
|
|
r = requests.get(url, headers=headers)
|
|
data = r.json()
|
|
values = data.get("data", {}).get("valueRange", {}).get("values", [])
|
|
return values
|
|
|
|
def write_cell_range(range_str, values_list):
|
|
"""Write values to a range"""
|
|
url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values"
|
|
headers = {
|
|
"Authorization": f"Bearer {FEISHU_TOKEN}",
|
|
"Content-Type": "application/json"
|
|
}
|
|
body = {
|
|
"valueRange": {
|
|
"range": f"{SHEET_ID}!{range_str}",
|
|
"values": values_list
|
|
}
|
|
}
|
|
r = requests.put(url, headers=headers, json=body)
|
|
resp = r.json()
|
|
if resp.get("code") != 0:
|
|
print(f" ERROR writing {range_str}: {resp}")
|
|
else:
|
|
print(f" OK: {range_str}")
|
|
return resp
|
|
|
|
def main():
|
|
# Step 1: Read sheet data
|
|
print("Reading sheet data...")
|
|
rows = get_sheet_data()
|
|
print(f"Got {len(rows)} rows")
|
|
|
|
# Parse rows with phones
|
|
phone_rows = [] # (row_num, phone, existing_uid, existing_d, existing_i, existing_j)
|
|
for i, row in enumerate(rows):
|
|
row_num = i + 3
|
|
if len(row) < 5:
|
|
continue
|
|
phone = row[4].strip() if row[4] else ""
|
|
if not phone or len(phone) != 11 or not phone.isdigit():
|
|
continue
|
|
uid = row[7].strip() if len(row) > 7 and row[7] else ""
|
|
d_val = row[3] if len(row) > 3 and row[3] else ""
|
|
i_val = row[8].strip() if len(row) > 8 and row[8] else ""
|
|
j_val = row[9].strip() if len(row) > 9 and row[9] else ""
|
|
phone_rows.append((row_num, phone, uid, d_val, i_val, j_val))
|
|
|
|
print(f"Found {len(phone_rows)} rows with valid phones")
|
|
|
|
# Separate into two groups:
|
|
# Group A: H is "未注册" or empty → need to match and fill H, D, I, J
|
|
# Group B: H has valid numeric UID but D is empty → need to fill D, I, J
|
|
group_a = [] # need phone matching
|
|
group_b = [] # already have UID, need D/I/J
|
|
|
|
for row_num, phone, uid, d_val, i_val, j_val in phone_rows:
|
|
if not uid or uid == "未注册":
|
|
group_a.append((row_num, phone, uid, d_val, i_val, j_val))
|
|
elif uid.isdigit():
|
|
if not d_val or not i_val or not j_val:
|
|
group_b.append((row_num, phone, uid, d_val, i_val, j_val))
|
|
|
|
print(f"Group A (need phone match): {len(group_a)}")
|
|
print(f"Group B (have UID, need D/I/J): {len(group_b)}")
|
|
|
|
# Step 2: Encrypt all phones and query PostgreSQL
|
|
all_phones_a = [p[1] for p in group_a]
|
|
all_uids_b = [p[2] for p in group_b]
|
|
|
|
conn = psycopg2.connect(**PG_CONFIG)
|
|
cur = conn.cursor()
|
|
|
|
# Map: encrypted_phone -> account_id
|
|
enc_to_uid = {}
|
|
if all_phones_a:
|
|
enc_list = [encrypt_phone(p) for p in all_phones_a]
|
|
phone_to_enc = dict(zip(all_phones_a, enc_list))
|
|
|
|
# Query accounts
|
|
placeholders = ",".join(["%s"] * len(enc_list))
|
|
cur.execute(
|
|
f"SELECT id, tel_encrypt FROM bi_vala_app_account WHERE tel_encrypt IN ({placeholders}) AND status=1 AND deleted_at IS NULL",
|
|
enc_list
|
|
)
|
|
for row in cur.fetchall():
|
|
uid, tel_enc = row
|
|
enc_to_uid[tel_enc] = uid
|
|
|
|
print(f"Matched {len(enc_to_uid)} phones in bi_vala_app_account")
|
|
|
|
# For matched accounts, get registration date and download channel
|
|
matched_uids = list(enc_to_uid.values())
|
|
uid_to_info = {}
|
|
if matched_uids:
|
|
placeholders2 = ",".join(["%s"] * len(matched_uids))
|
|
cur.execute(
|
|
f"SELECT id, created_at::date, download_channel FROM bi_vala_app_account WHERE id IN ({placeholders2}) AND status=1 AND deleted_at IS NULL",
|
|
matched_uids
|
|
)
|
|
for row in cur.fetchall():
|
|
uid_to_info[row[0]] = (str(row[1]) if row[1] else "", row[2] or "")
|
|
|
|
# Step 3: Get trial lesson counts for ALL accounts (both groups)
|
|
all_uids = list(set(
|
|
[enc_to_uid[encrypt_phone(p[1])] for p in group_a if encrypt_phone(p[1]) in enc_to_uid] +
|
|
[p[2] for p in group_b]
|
|
))
|
|
|
|
uid_to_trial_count = {}
|
|
if all_uids:
|
|
placeholders3 = ",".join(["%s"] * len(all_uids))
|
|
cur.execute(
|
|
f"SELECT account_id, COUNT(*) FROM bi_user_course_detail WHERE account_id IN ({placeholders3}) AND expire_time IS NULL AND deleted_at IS NULL GROUP BY account_id",
|
|
all_uids
|
|
)
|
|
for row in cur.fetchall():
|
|
uid_to_trial_count[row[0]] = row[1]
|
|
|
|
cur.close()
|
|
conn.close()
|
|
|
|
print(f"Trial counts: {len(uid_to_trial_count)} accounts have trials")
|
|
print(f"Account info: {len(uid_to_info)} accounts have reg date/channel")
|
|
|
|
# Step 4: Build write batches
|
|
# For Group A: write H (UID), D (trial count), I (reg date), J (channel)
|
|
# For Group B: write D (trial count), I (reg date), J (channel)
|
|
|
|
writes_h = [] # (row, value)
|
|
writes_d = [] # (row, value)
|
|
writes_i = [] # (row, value)
|
|
writes_j = [] # (row, value)
|
|
|
|
matched_count = 0
|
|
|
|
for row_num, phone, old_uid, old_d, old_i, old_j in group_a:
|
|
enc = encrypt_phone(phone)
|
|
uid = enc_to_uid.get(enc)
|
|
if uid:
|
|
matched_count += 1
|
|
uid_str = str(uid)
|
|
# H column: UID
|
|
writes_h.append((row_num, uid_str))
|
|
# D column: trial count
|
|
trial = uid_to_trial_count.get(uid, 0)
|
|
writes_d.append((row_num, str(trial) if trial else ""))
|
|
# I column: reg date
|
|
info = uid_to_info.get(uid, ("", ""))
|
|
writes_i.append((row_num, info[0]))
|
|
# J column: channel
|
|
writes_j.append((row_num, info[1]))
|
|
# If not matched, skip - don't write anything
|
|
|
|
for row_num, phone, uid_str, old_d, old_i, old_j in group_b:
|
|
uid = int(uid_str)
|
|
# D column: trial count
|
|
trial = uid_to_trial_count.get(uid, 0)
|
|
writes_d.append((row_num, str(trial) if trial else ""))
|
|
# I column: reg date
|
|
info = uid_to_info.get(uid, ("", ""))
|
|
writes_i.append((row_num, info[0]))
|
|
# J column: channel
|
|
writes_j.append((row_num, info[1]))
|
|
|
|
print(f"\nMatched {matched_count} new phones")
|
|
print(f"Total H writes: {len(writes_h)}")
|
|
print(f"Total D writes: {len(writes_d)}")
|
|
print(f"Total I writes: {len(writes_i)}")
|
|
print(f"Total J writes: {len(writes_j)}")
|
|
|
|
# Step 5: Write in batches (consecutive rows per column)
|
|
def write_batch(writes, col_letter):
|
|
if not writes:
|
|
return
|
|
# Sort by row number
|
|
writes.sort(key=lambda x: x[0])
|
|
# Group consecutive rows
|
|
i = 0
|
|
while i < len(writes):
|
|
start = writes[i][0]
|
|
vals = []
|
|
j = i
|
|
while j < len(writes) and writes[j][0] == start + (j - i):
|
|
vals.append([writes[j][1]])
|
|
j += 1
|
|
end = start + len(vals) - 1
|
|
range_str = f"{col_letter}{start}:{col_letter}{end}"
|
|
write_cell_range(range_str, vals)
|
|
i = j
|
|
time.sleep(0.05)
|
|
|
|
print("\nWriting H column...")
|
|
write_batch(writes_h, "H")
|
|
|
|
print("\nWriting D column...")
|
|
write_batch(writes_d, "D")
|
|
|
|
print("\nWriting I column...")
|
|
write_batch(writes_i, "I")
|
|
|
|
print("\nWriting J column...")
|
|
write_batch(writes_j, "J")
|
|
|
|
print("\n=== SUMMARY ===")
|
|
print(f"Phones matched: {matched_count}")
|
|
print(f"H (UID) written: {len(writes_h)}")
|
|
print(f"D (trial count) written: {len(writes_d)}")
|
|
print(f"I (reg date) written: {len(writes_i)}")
|
|
print(f"J (channel) written: {len(writes_j)}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|