"""BossAi 数据库初始化:建表 + 索引 + 种子数据。 运行:python3 db_init.py 幂等:重复执行不会报错,已存在的表跳过。 """ import psycopg2 from datetime import datetime, timezone SCHEMA = """ -- 法规知识库:法规/案例/模板,支持 RAG 检索 CREATE TABLE IF NOT EXISTS knowledge_docs ( id SERIAL PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL, source TEXT NOT NULL DEFAULT 'local', doc_type TEXT NOT NULL DEFAULT 'law', -- law / case / template region TEXT, -- 适用地区,如 全国 / 上海 effective_date DATE, -- 生效日期(法规) status TEXT NOT NULL DEFAULT 'active', -- active / superseded / repealed keywords TEXT, -- 逗号分隔关键词 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_knowledge_docs_type ON knowledge_docs(doc_type); CREATE INDEX IF NOT EXISTS idx_knowledge_docs_status ON knowledge_docs(status); CREATE INDEX IF NOT EXISTS idx_knowledge_docs_keywords ON knowledge_docs USING gin(to_tsvector('simple', keywords)); -- 用户设置:单用户 MVP,key-value 存储 CREATE TABLE IF NOT EXISTS user_settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- 专家分配规则:风险等级 → 专家类型 + SLA 时长(小时) CREATE TABLE IF NOT EXISTS expert_rules ( id SERIAL PRIMARY KEY, risk_level TEXT NOT NULL UNIQUE, -- green / amber / orange / red expert_type TEXT NOT NULL, -- 律师 / 资深律师 / 税务师 / 会计师 sla_hours INTEGER NOT NULL DEFAULT 24, -- SLA 响应时长(小时) auto_assign TEXT, -- 自动分配的专家姓名(可空) created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- 人工复核工单 CREATE TABLE IF NOT EXISTS review_tickets ( id SERIAL PRIMARY KEY, ticket_no TEXT NOT NULL UNIQUE, -- RT-YYYYMMDD-NNN session_id TEXT, -- 来源会话(脱敏后可空) question TEXT NOT NULL, -- 用户原始问题(脱敏) risk_level TEXT NOT NULL, -- green / amber / orange / red conclusion TEXT, -- AI 结论摘要 contact TEXT, -- 用户联系方式(脱敏) status TEXT NOT NULL DEFAULT 'pending', -- pending / assigned / resolved / closed priority TEXT NOT NULL DEFAULT 'normal', -- normal / urgent assignee TEXT, -- 分配的律师/专家 expert_type TEXT, -- 专家类型(来自规则) resolution TEXT, -- 处理结果 sla_deadline TIMESTAMPTZ, -- SLA 截止时间 sla_status TEXT NOT NULL DEFAULT 'normal', -- normal / warning / overdue created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_review_tickets_status ON review_tickets(status); CREATE INDEX IF NOT EXISTS idx_review_tickets_priority ON review_tickets(priority); CREATE INDEX IF NOT EXISTS idx_review_tickets_created ON review_tickets(created_at DESC); -- 用户画像:按手机号累积用户特征(非对话内容),用于个性化回复 CREATE TABLE IF NOT EXISTS user_profiles ( phone TEXT PRIMARY KEY, profile JSONB NOT NULL DEFAULT '{}', -- 画像数据(行业/规模/偏好/常用话题等) interaction_count INTEGER NOT NULL DEFAULT 0, -- 交互次数 last_active TIMESTAMPTZ, -- 最后活跃时间 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- 用户画像历史版本:每次更新时写入快照,支持版本回溯 CREATE TABLE IF NOT EXISTS user_profile_history ( id SERIAL PRIMARY KEY, phone TEXT NOT NULL, version INTEGER NOT NULL, -- 版本号(递增) profile JSONB NOT NULL DEFAULT '{}', -- 画像快照 source TEXT NOT NULL DEFAULT 'auto', -- auto(AI提取) / manual(用户确认) created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_profile_history_phone ON user_profile_history(phone); CREATE INDEX IF NOT EXISTS idx_profile_history_phone_version ON user_profile_history(phone, version DESC); -- 用户账号:手机号+密码注册登录 CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, phone TEXT NOT NULL UNIQUE, -- 手机号(登录账号) password TEXT NOT NULL, -- bcrypt 哈希 name TEXT, -- 昵称(可选) status TEXT NOT NULL DEFAULT 'active', -- active/inactive(管理员终止) created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- 专家库:按分类上架的专家(法务/经营/情绪/生活) CREATE TABLE IF NOT EXISTS experts ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, -- 专家姓名 phone TEXT NOT NULL UNIQUE, -- 手机号(专家登录账号) password TEXT NOT NULL, -- bcrypt 哈希 category TEXT NOT NULL, -- 分类:legal/decision/emotion/life title TEXT, -- 职称/头衔(如 高级律师/注册会计师) bio TEXT, -- 简介 expertise TEXT, -- 擅长领域(逗号分隔) avatar_url TEXT, -- 头像 URL(可空) rate INTEGER NOT NULL DEFAULT 0, -- 服务评分(0-500,除以100为星级) rating_count INTEGER NOT NULL DEFAULT 0, -- 评分人数 status TEXT NOT NULL DEFAULT 'active', -- active(上架) / inactive(下架) created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_experts_category ON experts(category); CREATE INDEX IF NOT EXISTS idx_experts_status ON experts(status); -- 专家服务关系:用户选定专家,专家同意后建立服务关系 CREATE TABLE IF NOT EXISTS expert_services ( id SERIAL PRIMARY KEY, user_phone TEXT NOT NULL, -- 用户手机号 expert_id INTEGER NOT NULL REFERENCES experts(id), category TEXT NOT NULL, -- 分类 status TEXT NOT NULL DEFAULT 'pending', -- pending(待确认) / active(服务中) / closed(已结束) signed_at TIMESTAMPTZ, -- 专家确认时间 closed_at TIMESTAMPTZ, -- 结束时间 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(user_phone, expert_id) -- 同一用户同一专家只能有一条服务关系 ); CREATE INDEX IF NOT EXISTS idx_expert_services_user ON expert_services(user_phone); CREATE INDEX IF NOT EXISTS idx_expert_services_expert ON expert_services(expert_id); CREATE INDEX IF NOT EXISTS idx_expert_services_status ON expert_services(status); -- 专家咨询:用户将与 AI 的对话话题发给专家 CREATE TABLE IF NOT EXISTS expert_consultations ( id SERIAL PRIMARY KEY, service_id INTEGER NOT NULL REFERENCES expert_services(id), user_phone TEXT NOT NULL, expert_id INTEGER NOT NULL REFERENCES experts(id), category TEXT NOT NULL, topic TEXT NOT NULL, -- 咨询话题(来自 AI 对话摘要) context TEXT, -- 对话上下文(脱敏后的摘要) question TEXT, -- 用户给专家的具体问题 reply TEXT, -- 专家回复 status TEXT NOT NULL DEFAULT 'pending', -- pending(待回复) / replied(已回复) / closed(已关闭) replied_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_expert_consultations_service ON expert_consultations(service_id); CREATE INDEX IF NOT EXISTS idx_expert_consultations_expert ON expert_consultations(expert_id); CREATE INDEX IF NOT EXISTS idx_expert_consultations_status ON expert_consultations(status); -- 专家服务评价 CREATE TABLE IF NOT EXISTS expert_ratings ( id SERIAL PRIMARY KEY, consultation_id INTEGER NOT NULL REFERENCES expert_consultations(id), user_phone TEXT NOT NULL, expert_id INTEGER NOT NULL REFERENCES experts(id), rating INTEGER NOT NULL, -- 评分 1-5 comment TEXT, -- 评价内容 created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_expert_ratings_expert ON expert_ratings(expert_id); -- 管理员表(独立于用户和专家) CREATE TABLE IF NOT EXISTS admins ( id SERIAL PRIMARY KEY, phone TEXT NOT NULL UNIQUE, -- 手机号(登录账号) password TEXT NOT NULL, -- bcrypt 哈希 name TEXT, -- 姓名 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- 对话历史:普通模式明文存储,保密模式加密存储,阅后即焚不落库 CREATE TABLE IF NOT EXISTS chat_messages ( id SERIAL PRIMARY KEY, session_id TEXT NOT NULL, -- 会话 ID(内存会话标识) phone TEXT NOT NULL, -- 用户手机号 mode TEXT NOT NULL, -- normal / confidential(ephemeral 不落库) role TEXT NOT NULL, -- user / assistant content TEXT NOT NULL, -- 消息正文(保密模式为密文) risk_data JSONB NOT NULL DEFAULT '{}', -- 风险卡(level/conclusion/actions/evidence/escalation) meta_data JSONB NOT NULL DEFAULT '{}', -- 元数据(scene/inference_location/model/fallback_used) created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id); CREATE INDEX IF NOT EXISTS idx_chat_messages_phone ON chat_messages(phone); CREATE INDEX IF NOT EXISTS idx_chat_messages_phone_mode ON chat_messages(phone, mode); CREATE INDEX IF NOT EXISTS idx_chat_messages_created ON chat_messages(created_at DESC); -- ===== BossAi 2.0 新增表 ===== -- 案卡:每次咨询绑定一张案卡,可视化追踪全生命周期 CREATE TABLE IF NOT EXISTS matters ( id SERIAL PRIMARY KEY, code TEXT NOT NULL UNIQUE, user_phone TEXT NOT NULL, scene TEXT NOT NULL, title TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'new', session_id TEXT, risk_level TEXT, summary TEXT, closed_at TIMESTAMPTZ, execution_result TEXT, execution_note TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_matters_user_phone ON matters(user_phone); CREATE INDEX IF NOT EXISTS idx_matters_status ON matters(status); CREATE INDEX IF NOT EXISTS idx_matters_user_status ON matters(user_phone, status); CREATE INDEX IF NOT EXISTS idx_matters_created ON matters(created_at DESC); -- 专家关系:用户咨询过的专家自动沉淀为"我的团队" CREATE TABLE IF NOT EXISTS expert_relations ( id SERIAL PRIMARY KEY, user_phone TEXT NOT NULL, expert_id INTEGER NOT NULL REFERENCES experts(id), consultation_count INTEGER NOT NULL DEFAULT 0, last_consulted_at TIMESTAMPTZ, last_topic TEXT, tags TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(user_phone, expert_id) ); CREATE INDEX IF NOT EXISTS idx_expert_relations_user ON expert_relations(user_phone); CREATE INDEX IF NOT EXISTS idx_expert_relations_expert ON expert_relations(expert_id); -- 风险推送:AI 主动提醒用户行业新规、合同风险等 CREATE TABLE IF NOT EXISTS risk_alerts ( id SERIAL PRIMARY KEY, user_phone TEXT NOT NULL, level TEXT NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, source TEXT NOT NULL DEFAULT 'knowledge', related_matter_id INTEGER, read_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_risk_alerts_user ON risk_alerts(user_phone); CREATE INDEX IF NOT EXISTS idx_risk_alerts_user_read ON risk_alerts(user_phone, read_at); CREATE INDEX IF NOT EXISTS idx_risk_alerts_created ON risk_alerts(created_at DESC); -- 每日热点问题库:每天凌晨 AI 生成,管理员可手动更新 CREATE TABLE IF NOT EXISTS daily_topics ( id SERIAL PRIMARY KEY, date DATE NOT NULL, -- 生效日期 scene TEXT NOT NULL, -- legal/decision/life/emotion questions JSONB NOT NULL, -- 问题数组 ["问题1","问题2",...] source TEXT NOT NULL DEFAULT 'ai', -- ai(自动生成) / admin(管理员手动) created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- 唯一约束:同一天同一场景只有一条记录(更新时覆盖) CREATE UNIQUE INDEX IF NOT EXISTS idx_daily_topics_date_scene ON daily_topics(date, scene); CREATE INDEX IF NOT EXISTS idx_daily_topics_date ON daily_topics(date DESC); -- 示例问题点击埋点:记录用户点击哪些示例问题 CREATE TABLE IF NOT EXISTS sample_clicks ( id SERIAL PRIMARY KEY, user_phone TEXT, scene TEXT NOT NULL, -- legal/decision/life/emotion question TEXT NOT NULL, -- 被点击的示例问题 clicked_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_sample_clicks_scene ON sample_clicks(scene); CREATE INDEX IF NOT EXISTS idx_sample_clicks_clicked ON sample_clicks(clicked_at DESC); """ # 专家种子数据(密码统一为 expert123,生产环境应让专家自行注册) SEED_EXPERTS = [ # 法务(legal) ("张明律师", "legal", "高级律师", "10年中小企业法务经验,擅长劳动纠纷、合同审查、股权纠纷", "劳动用工,合同审查,股权纠纷,债权债务"), ("李华律师", "legal", "执业律师", "专注商事诉讼,擅长公司治理、税务合规", "公司治理,税务合规,商事诉讼"), # 经营(decision) ("王强顾问", "decision", "高级经营顾问", "15年企业管理咨询经验,擅长战略规划、组织架构、绩效管理", "战略规划,组织架构,绩效管理,扩张决策"), ("赵敏分析师", "decision", "财务分析师", "CPA持证,擅长财务诊断、融资规划、现金流管理", "财务诊断,融资规划,现金流管理"), # 情绪(emotion) ("陈静咨询师", "emotion", "国家二级心理咨询师", "专注创业者心理健康,擅长焦虑疏导、压力管理、决策疲劳", "焦虑疏导,压力管理,决策疲劳,创业者心理"), ("刘阳教练", "emotion", "情绪教练", "10年情绪管理培训经验,擅长情绪识别、沟通改善", "情绪识别,沟通改善,团队氛围"), # 生活(life) ("周婷顾问", "life", "生活规划师", "擅长时间管理、家庭关系、健康管理", "时间管理,家庭关系,健康管理"), ("吴昊教练", "life", "商务礼仪教练", "专注商务社交、出差规划、形象管理", "商务礼仪,出差规划,形象管理"), ] # 管理员种子(密码 admin123) SEED_ADMINS = [ ("平台管理员", "13800000000"), ] SEED_KNOWLEDGE = [ # 法规(law) ("劳动合同法-试用期解除", "试用期解除需证明不符合录用条件,需有明确考核标准。依据《劳动合同法》第三十九条第一项。", "law", "全国", "2013-07-01", "试用期,解除,录用条件"), ("劳动合同法-工伤不得解除", "职工因工负伤,用人单位不得依据第四十条、四十一条解除劳动合同。依据《工伤保险条例》第三十三条。", "law", "全国", "2011-01-01", "工伤,解除,保护"), ("民法典-违约金上限", "违约金过分高于造成损失的,当事人可请求法院或仲裁机构予以适当减少。一般以实际损失30%为参考。依据《民法典》第五百八十五条。", "law", "全国", "2021-01-01", "违约金,减少,实际损失"), # 案例(case) ("劳动仲裁案例-旷工开除", "某公司以旷工3天为由开除员工,因考勤制度未经公示且未履行告知程序,被认定违法解除,赔偿2N。", "case", "上海", None, "旷工,开除,违法解除,赔偿"), ("股东出资瑕疵案例", "某股东未按期足额缴纳出资,公司及其他股东诉请补缴并赔偿违约损失,法院支持补缴并按章程追究违约责任。", "case", "全国", None, "股东,出资,违约,补缴"), # 模板(template) ("催款函模板", "致XX公司:贵司于X年X月X日欠付货款XX元,已逾期XX天。请于收到本函X日内付清,否则将采取法律措施。", "template", "全国", None, "催款,欠款,逾期"), ("解除劳动合同通知书模板", "致XX员工:因XX原因,公司决定于X年X月X日解除与您的劳动合同。经济补偿按法律规定执行。请于X日前办理交接。", "template", "全国", None, "解除,劳动合同,通知"), # 经营指南(guide) ("中小企业现金流管理要点", "现金流是中小企业生命线。建议:1.保持6个月运营资金储备;2.应收账款30天内催收;3.大额支出分批审批;4.每月做现金流预测。", "guide", "全国", None, "现金流,资金,应收账款,预测"), ("扩张前必须评估的5个指标", "扩张决策前评估:1.毛利率是否>30%;2.客户复购率;3.人均产出;4.现金流覆盖月数;5.团队管理带宽。任一不达标建议暂缓。", "guide", "全国", None, "扩张,评估,毛利率,现金流"), ("降价策略与利润平衡", "降价前评估:1.需求价格弹性(弹性>1降价有效);2.竞品定价区间;3.成本结构是否允许;4.是否引发价格战。降价不等于增收。", "guide", "全国", None, "降价,定价,利润,竞争"), # 行业知识(industry) ("餐饮行业用工特点", "餐饮行业用工特点:1.员工流动率高(年均40-60%);2.兼职占比高;3.社保合规风险大;4.工伤风险高(烫伤/切伤)。建议完善考勤和安全培训。", "industry", "全国", None, "餐饮,用工,流动率,社保,工伤"), ("制造业常见法律风险", "制造业常见风险:1.环保处罚(废水废气);2.安全生产责任;3.供应链合同纠纷;4.知识产权(技术泄露);5.劳动密集型用工合规。", "industry", "全国", None, "制造,环保,安全,知识产权,用工"), ("科技企业股权激励要点", "科技企业股权激励:1.期权池建议10-15%;2.行权周期4年+1年悬崖;3.离职回购条款;4.税务规划(递延纳税);5.与业绩挂钩。", "industry", "全国", None, "科技,股权,期权,激励,税务"), # 情绪疏导(emotion) ("创业者焦虑应对方法", '创业者焦虑常见且正常。应对:1.区分「能控制」和「不能控制」;2.每天15分钟正念呼吸;3.把大问题拆成今天能做的一步;4.找同行聊聊;5.严重时寻求专业帮助。', "emotion", "全国", None, "焦虑,创业,正念,应对"), ("决策疲劳的识别与恢复", "决策疲劳表现:拖延增多/易怒/回避选择。恢复:1.减少非必要决策(固定日程/穿搭);2.重要决策放在上午;3.充足睡眠;4.运动;5.委托授权。", "emotion", "全国", None, "决策疲劳,拖延,恢复,授权"), ("危机信号识别清单", '需立即关注的心理危机信号:1.持续失眠>2周;2.提到「没有意义」「结束一切」;3.社交完全退缩;4.食欲剧变;5.自伤念头。出现任一建议联系心理援助热线400-161-9995。', "emotion", "全国", None, "危机,失眠,自伤,援助热线"), # 生活常识(life) ("管理者时间管理法则", "时间管理:1.用四象限法区分重要/紧急;2.每天聚焦3件重要不紧急的事;3.批量处理邮件(早晚各一次);4.会议<30分钟;5.留白时间应对突发。", "life", "全国", None, "时间管理,四象限,效率,会议"), ("商务出差打包清单", "出差必备:1.证件(身份证/护照/机票);2.充电器+移动电源;3.商务正装1套;4.常备药(胃药/感冒药);5.名片;6.合同/文件复印件;7.保温杯。", "life", "全国", None, "出差,打包,清单,商务"), ("家庭沟通技巧", "创业者的家庭沟通:1.每周固定家庭时间(不谈工作);2.重大决策提前告知家人;3.倾听>建议;4.表达感谢而非理所当然;5.冲突时先冷静24小时。", "life", "全国", None, "家庭,沟通,倾听,冲突"), ] SEED_SETTINGS = [ ("show_inference_location", "true"), ("ephemeral_disable_copy", "true"), ("auto_destroy_on_exit", "true"), ("long_term_memory", "false"), ("knowledge_base_write", "false"), ("training_data_collection", "false"), ("cloud_call_allowed", "true"), ("pii_redact_on_transfer", "true"), ("daily_reminder", "true"), ("risk_alert", "true"), # 自动升级规则:检测到风险时是否自动创建复核工单 ("auto_escalate_red", "true"), ("auto_escalate_orange", "false"), ] # 专家分配规则:风险等级 → 专家类型 + SLA 时长 + 自动分配 SEED_EXPERT_RULES = [ ("green", "法务顾问", 72, None), ("amber", "法务顾问", 48, None), ("orange", "执业律师", 24, None), ("red", "资深律师", 4, None), ] def main(): conn = psycopg2.connect(host="localhost", port=5432, dbname="mcp", user="postgres") conn.autocommit = True cur = conn.cursor() # 建表 cur.execute(SCHEMA) # 升级已有表:补新列(IF NOT EXISTS 语义用 DO $$ 块) cur.execute(""" DO $$ BEGIN BEGIN ALTER TABLE review_tickets ADD COLUMN expert_type TEXT; EXCEPTION WHEN duplicate_column THEN NULL; END; BEGIN ALTER TABLE review_tickets ADD COLUMN sla_deadline TIMESTAMPTZ; EXCEPTION WHEN duplicate_column THEN NULL; END; BEGIN ALTER TABLE review_tickets ADD COLUMN sla_status TEXT NOT NULL DEFAULT 'normal'; EXCEPTION WHEN duplicate_column THEN NULL; END; BEGIN ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT 'active'; EXCEPTION WHEN duplicate_column THEN NULL; END; -- 2.0:chat_messages 关联案卡 BEGIN ALTER TABLE chat_messages ADD COLUMN matter_id INTEGER; EXCEPTION WHEN duplicate_column THEN NULL; END; END $$; """) # 迁移 user_profiles:device_id → phone(旧表字段名变更) cur.execute(""" DO $$ BEGIN -- 若存在 device_id 列但不存在 phone 列,则重命名 IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'user_profiles' AND column_name = 'device_id' ) AND NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'user_profiles' AND column_name = 'phone' ) THEN ALTER TABLE user_profiles RENAME COLUMN device_id TO phone; END IF; END $$; """) print("schema created + upgraded") # SLA 索引(列已确保存在后创建) cur.execute("CREATE INDEX IF NOT EXISTS idx_review_tickets_sla ON review_tickets(sla_status)") # 2.0:案卡关联索引 cur.execute("CREATE INDEX IF NOT EXISTS idx_chat_messages_matter ON chat_messages(matter_id)") # 清除所有旧知识数据(测试数据已废弃,重新种子) cur.execute("DELETE FROM knowledge_doc_versions") cur.execute("DELETE FROM knowledge_docs") print("cleared all old knowledge docs") # 种子知识库(7 类分类,幂等:标题存在则跳过) for title, content, doc_type, region, eff_date, keywords in SEED_KNOWLEDGE: cur.execute( "SELECT 1 FROM knowledge_docs WHERE title = %s", (title,) ) if not cur.fetchone(): cur.execute( "INSERT INTO knowledge_docs (title, content, source, doc_type, region, effective_date, keywords) " "VALUES (%s, %s, 'seed', %s, %s, %s, %s)", (title, content, doc_type, region, eff_date, keywords), ) print(f"seeded {len(SEED_KNOWLEDGE)} knowledge docs") # 种子设置(幂等) for key, value in SEED_SETTINGS: cur.execute( "INSERT INTO user_settings (key, value) VALUES (%s, %s) " "ON CONFLICT (key) DO NOTHING", (key, value), ) print(f"seeded {len(SEED_SETTINGS)} settings") # 清理旧版本的角色权限设置项(已废弃,系统只有老板一个用户) cur.execute( "DELETE FROM user_settings WHERE key LIKE 'perm_%%'" ) removed = cur.rowcount if removed: print(f"cleaned {removed} deprecated perm_ settings") # 种子专家规则(幂等) for risk_level, expert_type, sla_hours, auto_assign in SEED_EXPERT_RULES: cur.execute( "INSERT INTO expert_rules (risk_level, expert_type, sla_hours, auto_assign) " "VALUES (%s, %s, %s, %s) ON CONFLICT (risk_level) DO NOTHING", (risk_level, expert_type, sla_hours, auto_assign), ) print(f"seeded {len(SEED_EXPERT_RULES)} expert rules") # 种子专家(幂等:手机号存在则跳过) import bcrypt expert_password_hash = bcrypt.hashpw(b"expert123", bcrypt.gensalt()).decode() for idx, (name, category, title, bio, expertise) in enumerate(SEED_EXPERTS): # 生成唯一 11 位手机号:139 + 分类码(2位) + 序号(6位) phone_suffix = {"legal": "10", "decision": "20", "emotion": "30", "life": "40"} expert_phone = f"139{phone_suffix.get(category, '00')}{str(idx).zfill(6)}" cur.execute("SELECT 1 FROM experts WHERE name = %s", (name,)) if not cur.fetchone(): cur.execute( "INSERT INTO experts (name, phone, password, category, title, bio, expertise, status) " "VALUES (%s, %s, %s, %s, %s, %s, %s, 'active')", (name, expert_phone, expert_password_hash, category, title, bio, expertise), ) print(f"seeded {len(SEED_EXPERTS)} experts") # 种子管理员(幂等:手机号存在则跳过) admin_password_hash = bcrypt.hashpw(b"admin123", bcrypt.gensalt()).decode() for name, phone in SEED_ADMINS: cur.execute("SELECT 1 FROM admins WHERE phone = %s", (phone,)) if not cur.fetchone(): cur.execute( "INSERT INTO admins (name, phone, password) VALUES (%s, %s, %s)", (name, phone, admin_password_hash), ) print(f"seeded {len(SEED_ADMINS)} admins") # 升级已有工单:补 sla_deadline(基于 risk_level 规则) cur.execute( "UPDATE review_tickets SET sla_deadline = created_at + interval '24 hours' " "WHERE sla_deadline IS NULL" ) cur.close() conn.close() print("db_init done") if __name__ == "__main__": main()