Files
server-core/app/training/db.py
T
Pine 6de1b5a31b refactor(training): 培训数据层并入唯一总库 app.db(非运行态建表/种子,全列可空)
- 训练 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。
2026-08-24 20:31:57 +08:00

223 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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": "交流沙龙 · 1230 人小场", "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()