diff --git a/deploy/dev/seed.py b/deploy/dev/seed.py new file mode 100644 index 0000000..fe7f09d --- /dev/null +++ b/deploy/dev/seed.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python3 +""" +Dev seed script — populates the local database with test data. + +Usage: + python seed.py # insert seed data (skips if already seeded) + python seed.py --reset # delete seed data first, then re-insert + +Requires: docker CLI accessible, juwan-postgres container running. +All IDs use a fixed range (100_001 – 109_999) to avoid collision with +snowflake-generated production IDs. +""" + +import json +import subprocess +import sys + +CONTAINER = "juwan-postgres" +DB_USER = "postgres" +DB_NAME = "app" + +# All seed users share this password: test1234 +# bcrypt hash generated via pgcrypto crypt() +PASSWORD = "test1234" + + +def psql(sql: str) -> str: + r = subprocess.run( + ["docker", "exec", "-i", CONTAINER, "psql", "-U", DB_USER, "-d", DB_NAME, + "-v", "ON_ERROR_STOP=1", "--no-psqlrc", "-t", "-A"], + input=sql, capture_output=True, text=True, timeout=30, + ) + if r.returncode != 0: + print(f"psql error:\n{r.stderr}", file=sys.stderr) + sys.exit(1) + return r.stdout.strip() + + +def already_seeded() -> bool: + cnt = psql("SELECT count(*) FROM games WHERE id BETWEEN 100001 AND 109999;") + return int(cnt) > 0 + + +def reset(): + print("Resetting seed data …") + psql(""" +DELETE FROM wallet_transactions WHERE user_id BETWEEN 100001 AND 109999; +DELETE FROM wallets WHERE user_id BETWEEN 100001 AND 109999; +DELETE FROM comment_likes WHERE user_id BETWEEN 100001 AND 109999; +DELETE FROM post_likes WHERE user_id BETWEEN 100001 AND 109999; +DELETE FROM comments WHERE id BETWEEN 100001 AND 109999; +DELETE FROM posts WHERE id BETWEEN 100001 AND 109999; +DELETE FROM order_state_logs WHERE id BETWEEN 100001 AND 109999; +DELETE FROM orders WHERE id BETWEEN 100001 AND 109999; +DELETE FROM shop_invitations WHERE id BETWEEN 100001 AND 109999; +DELETE FROM shop_players WHERE shop_id BETWEEN 100001 AND 109999; +DELETE FROM player_services WHERE id BETWEEN 100001 AND 109999; +DELETE FROM shops WHERE id BETWEEN 100001 AND 109999; +DELETE FROM players WHERE id BETWEEN 100001 AND 109999; +DELETE FROM favorites WHERE id BETWEEN 100001 AND 109999; +DELETE FROM user_follows WHERE id BETWEEN 100001 AND 109999; +DELETE FROM user_verifications WHERE id BETWEEN 100001 AND 109999; +DELETE FROM user_preferences WHERE user_id BETWEEN 100001 AND 109999; +DELETE FROM users WHERE id BETWEEN 100001 AND 109999; +DELETE FROM games WHERE id BETWEEN 100001 AND 109999; +""") + + +def seed(): + print("Seeding …") + + # ── Games ────────────────────────────────────────────────────── + psql(""" +INSERT INTO games (id, name, icon, category, sort_order) VALUES + (100001, '英雄联盟', '🎮', 'MOBA', 1), + (100002, '王者荣耀', '👑', 'MOBA', 2), + (100003, 'VALORANT', '🔫', 'FPS', 3), + (100004, '永劫无间', '⚔️', 'ACT', 4), + (100005, '原神', '🌟', 'RPG', 5), + (100006, 'CS2', '💣', 'FPS', 6), + (100007, '绝地求生', '🪖', 'FPS', 7), + (100008, '和平精英', '🎯', 'FPS', 8); +""") + + # ── Users ────────────────────────────────────────────────────── + # password_hash via pgcrypto: crypt('test1234', gen_salt('bf')) + psql(f""" +INSERT INTO users (id, username, password_hash, email, nickname, avatar, bio, "current_role", verified_roles, verification_status) VALUES + (100001, 'player_lux', + crypt('{PASSWORD}', gen_salt('bf')), + 'lux@test.local', '光辉女郎', '', '王者段位代练,擅长中单法师', + 'player', ARRAY['consumer','player'], + '{{"consumer":"approved","player":"approved"}}'::jsonb), + + (100002, 'player_yasuo', + crypt('{PASSWORD}', gen_salt('bf')), + 'yasuo@test.local', '疾风剑豪', '', '国服亚索,上分快准狠', + 'player', ARRAY['consumer','player'], + '{{"consumer":"approved","player":"approved"}}'::jsonb), + + (100003, 'player_jett', + crypt('{PASSWORD}', gen_salt('bf')), + 'jett@test.local', '飞刀小姐', '', 'VALORANT 不朽段位', + 'player', ARRAY['consumer','player'], + '{{"consumer":"approved","player":"approved"}}'::jsonb), + + (100004, 'owner_star', + crypt('{PASSWORD}', gen_salt('bf')), + 'star@test.local', '星辰工作室', '', '专业代练工作室,诚信经营', + 'owner', ARRAY['consumer','owner'], + '{{"consumer":"approved","owner":"approved"}}'::jsonb), + + (100005, 'owner_wolf', + crypt('{PASSWORD}', gen_salt('bf')), + 'wolf@test.local', '狼群电竞', '', '高端局代练团队', + 'owner', ARRAY['consumer','owner'], + '{{"consumer":"approved","owner":"approved"}}'::jsonb), + + (100006, 'consumer_test', + crypt('{PASSWORD}', gen_salt('bf')), + 'consumer@test.local', '普通玩家小明', '', '想上钻石', + 'consumer', ARRAY['consumer'], + '{{"consumer":"approved"}}'::jsonb), + + (100007, 'consumer_test2', + crypt('{PASSWORD}', gen_salt('bf')), + 'consumer2@test.local', '快乐玩家小红', '', '想找人带上分', + 'consumer', ARRAY['consumer'], + '{{"consumer":"approved"}}'::jsonb), + + (100008, 'player_owner_duo', + crypt('{PASSWORD}', gen_salt('bf')), + 'duo@test.local', '全能选手', '', '既是打手也是店主', + 'player', ARRAY['consumer','player','owner'], + '{{"consumer":"approved","player":"approved","owner":"approved"}}'::jsonb); +""") + + # ── User preferences ─────────────────────────────────────────── + psql(""" +INSERT INTO user_preferences (user_id) VALUES + (100001),(100002),(100003),(100004),(100005),(100006),(100007),(100008); +""") + + # ── User verifications ───────────────────────────────────────── + psql(""" +INSERT INTO user_verifications (id, user_id, role, status, materials, reviewed_by, reviewed_at) VALUES + (100001, 100001, 'player', 'approved', '{"id_card":"mock://id1.jpg","rank_screenshot":"mock://rank1.jpg"}'::jsonb, 702627789228081152, NOW()), + (100002, 100002, 'player', 'approved', '{"id_card":"mock://id2.jpg","rank_screenshot":"mock://rank2.jpg"}'::jsonb, 702627789228081152, NOW()), + (100003, 100003, 'player', 'approved', '{"id_card":"mock://id3.jpg","rank_screenshot":"mock://rank3.jpg"}'::jsonb, 702627789228081152, NOW()), + (100004, 100004, 'owner', 'approved', '{"business_license":"mock://biz1.jpg"}'::jsonb, 702627789228081152, NOW()), + (100005, 100005, 'owner', 'approved', '{"business_license":"mock://biz2.jpg"}'::jsonb, 702627789228081152, NOW()), + (100006, 100008, 'player', 'approved', '{"id_card":"mock://id8.jpg","rank_screenshot":"mock://rank8.jpg"}'::jsonb, 702627789228081152, NOW()), + (100007, 100008, 'owner', 'approved', '{"business_license":"mock://biz8.jpg"}'::jsonb, 702627789228081152, NOW()); +""") + + # ── Players ──────────────────────────────────────────────────── + psql(""" +INSERT INTO players (id, user_id, status, rating, total_orders, completed_orders, gender, tags, games) VALUES + (100001, 100001, 'available', 4.85, 128, 120, false, ARRAY['中单','法师','上分快'], ARRAY[100001,100002]::bigint[]), + (100002, 100002, 'available', 4.72, 95, 88, true, ARRAY['上单','刺客','高端局'], ARRAY[100001]::bigint[]), + (100003, 100003, 'busy', 4.90, 67, 65, false, ARRAY['决斗','不朽段位'], ARRAY[100003]::bigint[]), + (100004, 100008, 'available', 4.60, 42, 38, true, ARRAY['全能','多游戏'], ARRAY[100001,100003,100005]::bigint[]); +""") + + # ── Shops ────────────────────────────────────────────────────── + psql(""" +INSERT INTO shops (id, owner_id, name, description, rating, total_orders, player_count, commission_type, commission_value, dispatch_mode, announcements) VALUES + (100001, 100004, '星辰代练工作室', '专业LOL/王者代练,7天无理由退款', 4.80, 256, 2, 'percentage', 15.00, 'manual', + ARRAY['新店开业,首单九折!','招募高端局打手,待遇从优']), + (100002, 100005, '狼群电竞俱乐部', '高端局代练团队,大师以上段位保证', 4.65, 180, 1, 'percentage', 12.00, 'auto', + ARRAY['本周特惠:钻石到大师只需199']), + (100003, 100008, '全能工作室', '多游戏代练,总有一款适合你', 4.50, 80, 1, 'fixed', 10.00, 'manual', + ARRAY['支持LOL/VALORANT/原神']); +""") + + # ── Shop players ─────────────────────────────────────────────── + psql(""" +INSERT INTO shop_players (shop_id, player_id, is_primary) VALUES + (100001, 100001, true), + (100001, 100002, false), + (100002, 100003, true), + (100003, 100004, true); +""") + + # Update players.shop_id cache + psql(""" +UPDATE players SET shop_id = 100001 WHERE id = 100001; +UPDATE players SET shop_id = 100001 WHERE id = 100002; +UPDATE players SET shop_id = 100002 WHERE id = 100003; +UPDATE players SET shop_id = 100003 WHERE id = 100004; +""") + + # ── Player services ──────────────────────────────────────────── + psql(""" +INSERT INTO player_services (id, player_id, game_id, title, description, price, unit, rank_range, availability) VALUES + (100001, 100001, 100001, 'LOL 钻石上分', '钻石到大师,稳定上分', 99.00, '段', '钻石→大师', ARRAY['周一至周五','晚间']), + (100002, 100001, 100001, 'LOL 排位陪玩', '钻石段位陪玩,轻松愉快', 30.00, '局', '钻石', ARRAY['全天']), + (100003, 100001, 100002, '王者荣耀 星耀上分', '星耀到王者,快速安全', 68.00, '段', '星耀→王者', ARRAY['全天']), + (100004, 100002, 100001, 'LOL 大师冲击', '大师到宗师,高端局专精', 199.00, '段', '大师→宗师', ARRAY['周末','晚间']), + (100005, 100002, 100001, 'LOL 定位赛代打', '10局定位赛,保底铂金', 50.00, '10局','定位赛', ARRAY['全天']), + (100006, 100003, 100003, 'VALORANT 不朽上分', '永恒到不朽,决斗位专精', 150.00, '段', '永恒→不朽', ARRAY['晚间','周末']), + (100007, 100003, 100003, 'VALORANT 陪玩', '不朽段位陪玩,教学向', 40.00, '局', '不朽', ARRAY['全天']), + (100008, 100004, 100001, 'LOL 黄金上分', '黄金到铂金,新手友好', 39.00, '段', '黄金→铂金', ARRAY['全天']), + (100009, 100004, 100003, 'VALORANT 白银上分', '白银到黄金,基础教学', 45.00, '段', '白银→黄金', ARRAY['全天']), + (100010, 100004, 100005, '原神深渊代打', '深渊12层满星', 80.00, '次', '深渊12层', ARRAY['全天']); +""") + + # ── Wallets ──────────────────────────────────────────────────── + psql(""" +INSERT INTO wallets (user_id, balance, frozen_balance) VALUES + (100001, 2580.00, 0.00), + (100002, 1890.00, 0.00), + (100003, 3200.00, 0.00), + (100004, 5600.00, 0.00), + (100005, 4200.00, 0.00), + (100006, 500.00, 0.00), + (100007, 300.00, 0.00), + (100008, 1500.00, 0.00); +""") + + # ── Orders ───────────────────────────────────────────────────── + psql(""" +INSERT INTO orders (id, consumer_id, player_id, shop_id, service_snapshot, status, total_price, note) VALUES + (100001, 100006, 100001, 100001, + '{"serviceId":100001,"title":"LOL 钻石上分","price":99.00,"unit":"段","gameName":"英雄联盟"}'::jsonb, + 'completed', 99.00, '希望快点上分'), + (100002, 100006, 100002, 100001, + '{"serviceId":100004,"title":"LOL 大师冲击","price":199.00,"unit":"段","gameName":"英雄联盟"}'::jsonb, + 'in_progress', 199.00, '大师冲宗师'), + (100003, 100007, 100003, 100002, + '{"serviceId":100006,"title":"VALORANT 不朽上分","price":150.00,"unit":"段","gameName":"VALORANT"}'::jsonb, + 'pending_accept', 150.00, null), + (100004, 100007, 100001, 100001, + '{"serviceId":100002,"title":"LOL 排位陪玩","price":30.00,"unit":"局","gameName":"英雄联盟"}'::jsonb, + 'completed', 30.00, '陪玩两局'), + (100005, 100006, 100004, 100003, + '{"serviceId":100010,"title":"原神深渊代打","price":80.00,"unit":"次","gameName":"原神"}'::jsonb, + 'pending_payment', 80.00, '深渊满星'); +""") + + # ── Order state logs ─────────────────────────────────────────── + psql(""" +INSERT INTO order_state_logs (id, order_id, from_status, to_status, action, actor_id, actor_role) VALUES + (100001, 100001, null, 'pending_payment', 'create', 100006, 'consumer'), + (100002, 100001, 'pending_payment', 'pending_accept', 'pay', 100006, 'consumer'), + (100003, 100001, 'pending_accept', 'in_progress', 'accept', 100001, 'player'), + (100004, 100001, 'in_progress', 'pending_close', 'finish', 100001, 'player'), + (100005, 100001, 'pending_close', 'completed', 'confirm', 100006, 'consumer'), + (100006, 100002, null, 'pending_payment', 'create', 100006, 'consumer'), + (100007, 100002, 'pending_payment', 'pending_accept', 'pay', 100006, 'consumer'), + (100008, 100002, 'pending_accept', 'in_progress', 'accept', 100002, 'player'), + (100009, 100003, null, 'pending_payment', 'create', 100007, 'consumer'), + (100010, 100003, 'pending_payment', 'pending_accept', 'pay', 100007, 'consumer'), + (100011, 100004, null, 'pending_payment', 'create', 100007, 'consumer'), + (100012, 100004, 'pending_payment', 'pending_accept', 'pay', 100007, 'consumer'), + (100013, 100004, 'pending_accept', 'in_progress', 'accept', 100001, 'player'), + (100014, 100004, 'in_progress', 'pending_close', 'finish', 100001, 'player'), + (100015, 100004, 'pending_close', 'completed', 'confirm', 100007, 'consumer'), + (100016, 100005, null, 'pending_payment', 'create', 100006, 'consumer'); +""") + + # ── Wallet transactions ──────────────────────────────────────── + psql(""" +INSERT INTO wallet_transactions (id, user_id, type, amount, balance_after, description, order_id) VALUES + (100001, 100006, 'topup', 500.00, 500.00, '充值', null), + (100002, 100006, 'payment', -99.00, 401.00, 'LOL 钻石上分', 100001), + (100003, 100001, 'income', 84.15, 2580.00, 'LOL 钻石上分(扣佣)', 100001), + (100004, 100006, 'payment', -199.00, 202.00, 'LOL 大师冲击', 100002), + (100005, 100007, 'topup', 300.00, 300.00, '充值', null), + (100006, 100007, 'payment', -30.00, 270.00, 'LOL 排位陪玩', 100004), + (100007, 100001, 'income', 25.50, 2605.50, 'LOL 排位陪玩(扣佣)', 100004); +""") + + # ── Community posts ──────────────────────────────────────────── + psql(""" +INSERT INTO posts (id, author_id, author_role, title, content, tags, like_count, comment_count) VALUES + (100001, 100001, 'player', '新赛季中单法师强度排行', + '新赛季中单法师的强度有了不少变化,这里给大家分析一下当前版本的T0-T2梯队。\n\nT0: 阿狸、辛德拉\nT1: 维克托、泽丽\nT2: 佐伊、妮蔻\n\n大家觉得还有哪些英雄被低估了?', + ARRAY['英雄联盟','攻略','中单'], 12, 3), + (100002, 100003, 'player', 'VALORANT 新地图攻略分享', + '新地图的几个关键烟点和架枪位分享给大家。决斗位在这张图上优势很大,特别是A点的peek角度。', + ARRAY['VALORANT','攻略','地图'], 8, 1), + (100003, 100004, 'owner', '星辰工作室招募公告', + '星辰代练工作室现招募以下段位打手:\n- LOL 大师及以上\n- 王者荣耀 王者50星以上\n\n待遇优厚,佣金比例可谈。有意者私信联系。', + ARRAY['招募','工作室'], 5, 2), + (100004, 100006, 'consumer', '求推荐靠谱的LOL代练', + '钻石卡了好久上不去,想找个靠谱的代练帮忙上大师。有推荐的吗?最好是有保障的工作室。', + ARRAY['英雄联盟','求推荐'], 3, 2), + (100005, 100002, 'player', '上分心得:如何从钻石到大师', + '很多人卡在钻石上不去,其实关键在于几个点:\n1. 英雄池不要太广,精通2-3个就够\n2. 对线期不要浪,稳住发育\n3. 多看小地图,把握团战时机\n\n希望对大家有帮助!', + ARRAY['英雄联盟','心得','上分'], 15, 4); +""") + + # ── Comments ─────────────────────────────────────────────────── + psql(""" +INSERT INTO comments (id, post_id, author_id, content, like_count) VALUES + (100001, 100001, 100002, '阿狸确实强,ban率太高了', 3), + (100002, 100001, 100006, '维克托感觉也是T0级别的', 1), + (100003, 100001, 100003, '法师版本确实舒服', 2), + (100004, 100002, 100001, '新地图确实好玩,决斗位优势大', 1), + (100005, 100003, 100001, '有兴趣,已私信', 0), + (100006, 100003, 100002, '星辰工作室不错,之前合作过', 2), + (100007, 100004, 100001, '可以看看我的主页,钻石到大师是我的强项', 1), + (100008, 100004, 100004, '欢迎来星辰工作室了解,我们有专业的代练团队', 0), + (100009, 100005, 100006, '写得好,学到了', 2), + (100010, 100005, 100007, '确实,英雄池精了比广好', 1), + (100011, 100005, 100001, '补充一点:心态也很重要,连跪就休息', 3), + (100012, 100005, 100003, '赞同,我也是靠精通少数英雄上的不朽', 1); +""") + + # ── Post likes ───────────────────────────────────────────────── + psql(""" +INSERT INTO post_likes (post_id, user_id) VALUES + (100001, 100002),(100001, 100003),(100001, 100006),(100001, 100007), + (100002, 100001),(100002, 100006), + (100003, 100001),(100003, 100002), + (100004, 100001),(100004, 100004), + (100005, 100001),(100005, 100003),(100005, 100006),(100005, 100007); +""") + + # ── Comment likes ────────────────────────────────────────────── + psql(""" +INSERT INTO comment_likes (comment_id, user_id) VALUES + (100001, 100001),(100001, 100006),(100001, 100007), + (100003, 100001),(100003, 100006), + (100006, 100001),(100006, 100004), + (100009, 100001),(100009, 100007), + (100011, 100002),(100011, 100006),(100011, 100007); +""") + + # ── User follows ─────────────────────────────────────────────── + psql(""" +INSERT INTO user_follows (id, follower_id, followee_id) VALUES + (100001, 100006, 100001), + (100002, 100006, 100002), + (100003, 100007, 100003), + (100004, 100007, 100001), + (100005, 100001, 100002), + (100006, 100002, 100001); +""") + + # ── Favorites ────────────────────────────────────────────────── + psql(""" +INSERT INTO favorites (id, user_id, target_type, target_id) VALUES + (100001, 100006, 'player', 100001), + (100002, 100006, 'shop', 100001), + (100003, 100007, 'player', 100003), + (100004, 100007, 'shop', 100002); +""") + + print("Done! Seed data inserted.") + print() + print("Test accounts (password: test1234):") + print(" player_lux — 打手,LOL/王者,星辰工作室成员") + print(" player_yasuo — 打手,LOL高端局,星辰工作室成员") + print(" player_jett — 打手,VALORANT,狼群俱乐部成员") + print(" owner_star — 店主,星辰代练工作室") + print(" owner_wolf — 店主,狼群电竞俱乐部") + print(" consumer_test — 普通消费者") + print(" consumer_test2 — 普通消费者") + print(" player_owner_duo — 打手+店主,全能工作室") + + +if __name__ == "__main__": + if "--reset" in sys.argv: + reset() + elif already_seeded(): + print("Seed data already exists. Use --reset to re-seed.") + sys.exit(0) + seed()