6de1b5a31b
- 训练 ORM 模型改名回旧表名(accounts/events/bookings/tests/courses/policy_logs/plan_logs/survey_logs) 并全列可空(兼容按部分字段插入);alembic 0003 丢弃重建并入唯一总库。 - training/db.py 指向 app.db、移除运行时 init_db;main.py lifespan 移除 init_db。 - seed.py 增培训种子(超管账号/排期/在线课程, 幂等)。 - 验证(真实库 migrate+seed+TestClient):auth/me、admin/*、park/*、training /api/health|events|courses 全 200。
223 lines
9.9 KiB
Python
223 lines
9.9 KiB
Python
"""SQLite 数据层(兼容原 mock 后端 JSON store,迁移用)
|
||
位于 server-core/app/training/,数据落在 server-core/data/opc.db(与平台库 app.db 并列)。
|
||
"""
|
||
import json
|
||
import os
|
||
import sqlite3
|
||
import time
|
||
import random
|
||
|
||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # server-core 根
|
||
DB_PATH = os.path.join(BASE_DIR, "serverdata", "data", "app.db")
|
||
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||
|
||
|
||
def get_conn():
|
||
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
||
conn.row_factory = sqlite3.Row
|
||
conn.execute("PRAGMA journal_mode=WAL")
|
||
return conn
|
||
|
||
|
||
def gen_id(prefix):
|
||
ts = time.time()
|
||
s = int(ts)
|
||
# base36 大写,模拟 JS genId
|
||
def b36(n):
|
||
if n == 0:
|
||
return "0"
|
||
d = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||
out = ""
|
||
while n:
|
||
out = d[n % 36] + out
|
||
n //= 36
|
||
return out
|
||
return prefix + b36(s).upper() + b36(random.randint(0, 35)).upper()
|
||
|
||
|
||
SCHEMA = """
|
||
CREATE TABLE IF NOT EXISTS accounts (
|
||
id TEXT PRIMARY KEY,
|
||
username TEXT UNIQUE,
|
||
wxid TEXT,
|
||
phone TEXT,
|
||
contact TEXT,
|
||
name TEXT,
|
||
avatar TEXT,
|
||
password TEXT,
|
||
phone_bound INTEGER DEFAULT 0,
|
||
identities TEXT,
|
||
created_at TEXT
|
||
);
|
||
CREATE TABLE IF NOT EXISTS events (
|
||
id TEXT PRIMARY KEY,
|
||
type TEXT,
|
||
mode TEXT DEFAULT 'offline',
|
||
title TEXT,
|
||
subtitle TEXT,
|
||
desc TEXT,
|
||
location TEXT,
|
||
host TEXT,
|
||
image TEXT,
|
||
link TEXT,
|
||
start_at TEXT,
|
||
duration_min INTEGER DEFAULT 90,
|
||
capacity INTEGER DEFAULT 0,
|
||
status TEXT DEFAULT 'open'
|
||
);
|
||
CREATE TABLE IF NOT EXISTS bookings (
|
||
id TEXT PRIMARY KEY,
|
||
created_at TEXT,
|
||
status TEXT DEFAULT 'pending',
|
||
username TEXT,
|
||
name TEXT,
|
||
contact TEXT,
|
||
status_label TEXT,
|
||
want TEXT,
|
||
event_id TEXT,
|
||
event_title TEXT,
|
||
event_start TEXT,
|
||
topics TEXT,
|
||
question TEXT,
|
||
source TEXT,
|
||
checkin_at TEXT,
|
||
audit_status TEXT DEFAULT 'pending'
|
||
);
|
||
CREATE TABLE IF NOT EXISTS tests (
|
||
id TEXT PRIMARY KEY,
|
||
created_at TEXT,
|
||
username TEXT,
|
||
type_code TEXT,
|
||
persona TEXT,
|
||
adapt_index INTEGER,
|
||
adapt_level TEXT,
|
||
tracks TEXT,
|
||
version TEXT
|
||
);
|
||
CREATE TABLE IF NOT EXISTS courses (
|
||
id TEXT PRIMARY KEY,
|
||
category TEXT,
|
||
level TEXT,
|
||
title TEXT,
|
||
subtitle TEXT,
|
||
desc TEXT,
|
||
price REAL DEFAULT 0,
|
||
status TEXT DEFAULT 'draft',
|
||
start_at TEXT,
|
||
end_at TEXT,
|
||
venue TEXT,
|
||
quota INTEGER DEFAULT 0,
|
||
image TEXT,
|
||
host TEXT,
|
||
created_at TEXT
|
||
);
|
||
CREATE TABLE IF NOT EXISTS policy_logs (
|
||
id TEXT PRIMARY KEY,
|
||
created_at TEXT,
|
||
username TEXT,
|
||
answers TEXT,
|
||
policies_count INTEGER,
|
||
subsidies_count INTEGER,
|
||
loans_count INTEGER,
|
||
summary TEXT
|
||
);
|
||
CREATE TABLE IF NOT EXISTS plan_logs (
|
||
id TEXT PRIMARY KEY,
|
||
created_at TEXT,
|
||
username TEXT,
|
||
region TEXT,
|
||
status TEXT,
|
||
need_park INTEGER,
|
||
has_staff INTEGER,
|
||
steps_count INTEGER
|
||
);
|
||
CREATE TABLE IF NOT EXISTS survey_logs (
|
||
id TEXT PRIMARY KEY,
|
||
created_at TEXT,
|
||
username TEXT,
|
||
source TEXT,
|
||
answers TEXT
|
||
);
|
||
"""
|
||
|
||
SEED_EVENTS = [
|
||
{"id": "E-F001", "type": "free", "mode": "online", "title": "AI 让民宿主每月省 10 小时", "subtitle": "民宿 / 文旅创业 · 公益课", "desc": "拆解民宿经营里最耗时的重复劳动——接单、排房、回复咨询。现场演示 AI 怎么把每周 10 小时的杂活接住,让你把时间还给体验和口碑。不卖课,点到即止。", "location": "视频号直播", "host": "云超服 · OPC 培训", "image": "https://picsum.photos/seed/opc-f1/600/400", "link": "", "start_at": "2026-09-04T19:30:00+08:00", "duration_min": 90, "capacity": 0, "status": "open"},
|
||
{"id": "E-S001", "type": "salon", "mode": "offline", "title": "民宿 AI 化改造实战", "subtitle": "交流沙龙 · 12–30 人小场", "desc": "带上你的民宿,现场用 AI 搭建一套内容与客服小助手:从组标题、生成海报到自动回复,走通一遍。1 对 1 快诊你的具体场景,带走可复用的模板。", "location": "昆明市大学生创业园", "host": "云超服 · OPC 培训", "image": "https://picsum.photos/seed/opc-s1/600/400", "link": "", "start_at": "2026-09-05T14:00:00+08:00", "duration_min": 180, "capacity": 30, "status": "invite"},
|
||
{"id": "E-F002", "type": "free", "mode": "offline", "title": "用 AI 做云南咖啡品牌", "subtitle": "咖啡 / 农特产创业 · 公益课", "desc": "从云南豆讲到品牌:怎么用一个好故事、一套 AI 内容打法,把咖啡从产地卖到全国。现场看一条内容从 0 到发布的完整流程。", "location": "昆明市大学生创业园", "host": "云超服 · OPC 培训", "image": "https://picsum.photos/seed/opc-f2/600/400", "link": "", "start_at": "2026-09-06T19:30:00+08:00", "duration_min": 90, "capacity": 60, "status": "open"},
|
||
{"id": "E-S002", "type": "salon", "mode": "offline", "title": "技能打包:把手艺变成产品", "subtitle": "交流沙龙 · 副业转型专场", "desc": "工作坊:把你的技能、经验和手艺,改成能标价、能交付、能复制的产品。现场分组拆解你的卡点,产出一版可落地的产品草图。", "location": "昆明市大学生创业园", "host": "云超服 · OPC 培训", "image": "https://picsum.photos/seed/opc-s2/600/400", "link": "", "start_at": "2026-09-07T14:00:00+08:00", "duration_min": 180, "capacity": 24, "status": "open"},
|
||
{"id": "E-F003", "type": "free", "mode": "online", "title": "一人公司财税避坑入门", "subtitle": "自由职业 / 小老板 · 公益课", "desc": "个体户、一人公司最容易踩的财税坑:发票、成本票、申报节点、核定与查账。用大白话讲清楚,让你不再为报税发愁。", "location": "视频号直播", "host": "云超服 · OPC 培训", "image": "https://picsum.photos/seed/opc-f3/600/400", "link": "", "start_at": "2026-09-09T19:30:00+08:00", "duration_min": 90, "capacity": 0, "status": "pending"},
|
||
{"id": "E-F004", "type": "free", "mode": "offline", "title": "你适不适合创业?先做一次 OPC 测评", "subtitle": "大众 · 公益课", "desc": "现场 30 题速测你的 OPC 适配度:独立、风险、自驱、单兵、AI 意愿、安全垫六维速览,带你读懂自己的类型码与适合的赛道。", "location": "昆明市大学生创业园", "host": "云超服 · OPC 培训", "image": "https://picsum.photos/seed/opc-f4/600/400", "link": "", "start_at": "2026-09-10T19:30:00+08:00", "duration_min": 90, "capacity": 60, "status": "open"},
|
||
{"id": "E-S003", "type": "salon", "mode": "offline", "title": "OPC 测评报告 1 对 1 解读专场", "subtitle": "交流沙龙 · 测评者组场", "desc": "每人 15 分钟快诊:读懂你的 4 字母类型码、适配 6 维和推荐赛道,现场把报告讲到能落地,并给出你的起点方向。", "location": "昆明市大学生创业园", "host": "云超服 · OPC 培训", "image": "https://picsum.photos/seed/opc-s3/600/400", "link": "", "start_at": "2026-09-11T14:00:00+08:00", "duration_min": 180, "capacity": 20, "status": "open"},
|
||
{"id": "E-S004", "type": "salon", "mode": "offline", "title": "资源对接夜:能提供 × 需要", "subtitle": "交流沙龙 · 综合场", "desc": "一张桌子,把你的「能提供」和「需要」摆上台面,现场匹配同行与本地资源。可能认识下一个合作伙伴,也可能接住第一单。", "location": "昆明市大学生创业园", "host": "云超服 · OPC 培训", "image": "https://picsum.photos/seed/opc-s4/600/400", "link": "", "start_at": "2026-09-12T14:00:00+08:00", "duration_min": 180, "capacity": 30, "status": "done"},
|
||
]
|
||
|
||
|
||
def now_iso():
|
||
from datetime import datetime, timezone
|
||
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||
|
||
|
||
SEED_COURSES = [
|
||
{"id": "C-001", "category": "AI应用", "level": "入门", "title": "AI 内容一键产出:从标题到海报", "subtitle": "公益课回放", "desc": "把组标题、写正文、配图、做海报这一条链路用 AI 跑通。", "price": 0, "status": "published", "start_at": "2026-09-04T19:30:00+08:00", "end_at": "", "venue": "视频号直播", "quota": 0, "image": "", "host": "云超服 · OPC 培训", "created_at": "2026-08-24T00:00:00Z"},
|
||
{"id": "C-002", "category": "创业", "level": "进阶", "title": "一人公司财税避坑进阶", "subtitle": "线上标准课", "desc": "个体户 / 一人公司:发票、成本票、申报节点、核定与查账。", "price": 499, "status": "draft", "start_at": "2026-09-20T19:30:00+08:00", "end_at": "", "venue": "视频号直播", "quota": 200, "image": "", "host": "云超服 · OPC 培训", "created_at": "2026-08-24T00:00:00Z"},
|
||
]
|
||
|
||
|
||
# 运行时建表/种子已移除:表由 alembic(ORM 模型 accounts/events/... )创建;种子经 scripts/db/seed.py。
|
||
|
||
# ---------------- 通用读写 helpers ----------------
|
||
|
||
def rows_to_list(rows):
|
||
return [dict(r) for r in rows]
|
||
|
||
|
||
def fetch_one(table, **kw):
|
||
conn = get_conn()
|
||
where = " AND ".join([f"{k}=?" for k in kw])
|
||
row = conn.execute(f"SELECT * FROM {table} WHERE {where}", list(kw.values())).fetchone()
|
||
conn.close()
|
||
return dict(row) if row else None
|
||
|
||
|
||
def fetch_by_id(table, rid):
|
||
return fetch_one(table, id=rid)
|
||
|
||
|
||
def list_all(table, order_by=None):
|
||
conn = get_conn()
|
||
sql = f"SELECT * FROM {table}"
|
||
if order_by:
|
||
sql += f" ORDER BY {order_by}"
|
||
rows = conn.execute(sql).fetchall()
|
||
conn.close()
|
||
return rows_to_list(rows)
|
||
|
||
|
||
def insert(table, data):
|
||
conn = get_conn()
|
||
keys = list(data.keys())
|
||
conn.execute(
|
||
f"INSERT INTO {table} ({','.join(keys)}) VALUES ({','.join(['?']*len(keys))})",
|
||
[data[k] for k in keys],
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
|
||
def update_row(table, rid, patch):
|
||
conn = get_conn()
|
||
keys = list(patch.keys())
|
||
conn.execute(
|
||
f"UPDATE {table} SET {','.join([f'{k}=?' for k in keys])} WHERE id=?",
|
||
[patch[k] for k in keys] + [rid],
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
|
||
def delete_row(table, rid):
|
||
conn = get_conn()
|
||
conn.execute(f"DELETE FROM {table} WHERE id=?", (rid,))
|
||
conn.commit()
|
||
conn.close()
|