v1.0.1-beta: MySQL迁移 + 用户体系 + 经营管理/任务/产品改版
This commit is contained in:
@@ -2,9 +2,11 @@ from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import sqlite3 # 保留用于数据迁移
|
||||
import mysql.connector
|
||||
|
||||
from flask import Flask, jsonify, render_template, request, send_file
|
||||
from flask import Flask, jsonify, render_template, request, send_file, session, redirect
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -21,145 +23,323 @@ app = Flask(
|
||||
template_folder=str(ROOT / "templates"),
|
||||
static_folder=str(ROOT / "static"),
|
||||
)
|
||||
app.secret_key = os.environ.get("SECRET_KEY", "opc-dev-secret-2026")
|
||||
|
||||
# ---------- 鉴权 ----------
|
||||
|
||||
def login_required(f):
|
||||
from functools import wraps
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
if "user_id" not in session:
|
||||
return jsonify({"error": "未登录"}), 401
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
|
||||
@app.route("/login")
|
||||
def login_page():
|
||||
return render_template("login.html")
|
||||
|
||||
|
||||
@app.route("/api/auth/login", methods=["POST"])
|
||||
def auth_login():
|
||||
data = request.get_json(force=True) or {}
|
||||
username = data.get("username", "").strip()
|
||||
password = data.get("password", "")
|
||||
conn = db()
|
||||
try:
|
||||
user = one(conn, "SELECT * FROM users WHERE username=?", (username,))
|
||||
if not user or not check_password_hash(user["password_hash"], password):
|
||||
return jsonify({"error": "用户名或密码错误"}), 401
|
||||
session["user_id"] = user["id"]
|
||||
session["username"] = user["username"]
|
||||
session["display_name"] = user["display_name"]
|
||||
session["role"] = user["role"]
|
||||
# 管理员可看所有工作台,OPC负责人看分配的工作台
|
||||
if user["role"] == "admin":
|
||||
session["tenants"] = ["科普·无界", "科研·无界", "医患·无界"]
|
||||
else:
|
||||
ut = rows(conn, "SELECT tenant FROM user_tenants WHERE user_id=?", (user["id"],))
|
||||
session["tenants"] = [x["tenant"] for x in ut]
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"user": {"id": user["id"], "username": user["username"], "display_name": user["display_name"], "role": user["role"]},
|
||||
"tenants": session["tenants"],
|
||||
})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.route("/api/auth/logout", methods=["POST"])
|
||||
def auth_logout():
|
||||
session.clear()
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@app.route("/api/auth/me")
|
||||
def auth_me():
|
||||
if "user_id" not in session:
|
||||
return jsonify({"logged_in": False})
|
||||
return jsonify({
|
||||
"logged_in": True,
|
||||
"user": {"id": session["user_id"], "username": session["username"], "display_name": session["display_name"], "role": session["role"]},
|
||||
"tenants": session.get("tenants", []),
|
||||
})
|
||||
|
||||
|
||||
# ---------- 业务 API ----------
|
||||
|
||||
|
||||
def db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
return mysql.connector.connect(
|
||||
host="127.0.0.1",
|
||||
port=3306,
|
||||
user="opc",
|
||||
password="opc123456",
|
||||
database="opc",
|
||||
charset="utf8mb4",
|
||||
collation="utf8mb4_unicode_ci",
|
||||
)
|
||||
|
||||
|
||||
def now():
|
||||
return datetime.utcnow().isoformat()
|
||||
|
||||
|
||||
def _exec(conn, sql, args=()):
|
||||
"""执行 SQL,自动将 ? 转为 MySQL 的 %s"""
|
||||
cur = conn.cursor(dictionary=True)
|
||||
cur.execute(sql.replace("?", "%s"), args)
|
||||
return cur
|
||||
|
||||
|
||||
def rows(conn, sql, args=()):
|
||||
return [dict(row) for row in conn.execute(sql, args).fetchall()]
|
||||
cur = _exec(conn, sql, args)
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
return rows
|
||||
|
||||
|
||||
def one(conn, sql, args=()):
|
||||
row = conn.execute(sql, args).fetchone()
|
||||
return dict(row) if row else None
|
||||
cur = _exec(conn, sql, args)
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
return row
|
||||
|
||||
|
||||
def init_db():
|
||||
conn = db()
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS sales_leads (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
target_customer TEXT NOT NULL,
|
||||
priority TEXT NOT NULL DEFAULT 'P1',
|
||||
status TEXT NOT NULL DEFAULT '待跟进',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS follow_up_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
target_type TEXT NOT NULL,
|
||||
target_id INTEGER NOT NULL,
|
||||
followed_at TEXT NOT NULL DEFAULT '',
|
||||
follower TEXT NOT NULL DEFAULT '慰心',
|
||||
follow_up_method TEXT NOT NULL DEFAULT '记录',
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
next_action TEXT NOT NULL DEFAULT '',
|
||||
next_follow_up_at TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS business_proposals (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
customer_or_project_name TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT '草稿',
|
||||
created_date TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS operation_projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_name TEXT NOT NULL,
|
||||
project_version TEXT NOT NULL DEFAULT 'v1.0',
|
||||
project_type TEXT NOT NULL DEFAULT 'opportunity',
|
||||
project_status TEXT NOT NULL DEFAULT '',
|
||||
current_stage TEXT NOT NULL DEFAULT '',
|
||||
owner TEXT NOT NULL DEFAULT '慰心',
|
||||
start_date TEXT NOT NULL DEFAULT '',
|
||||
end_date TEXT NOT NULL DEFAULT '',
|
||||
target_customer TEXT NOT NULL DEFAULT '',
|
||||
customer_need TEXT NOT NULL DEFAULT '',
|
||||
expected_contract_amount REAL NOT NULL DEFAULT 0,
|
||||
expected_sign_date TEXT NOT NULL DEFAULT '',
|
||||
sign_probability REAL NOT NULL DEFAULT 0,
|
||||
next_action TEXT NOT NULL DEFAULT '',
|
||||
_exec(conn, """CREATE TABLE IF NOT EXISTS sales_leads (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
target_customer VARCHAR(1000) NOT NULL,
|
||||
priority VARCHAR(1000) NOT NULL DEFAULT 'P1',
|
||||
status VARCHAR(1000) NOT NULL DEFAULT '待跟进',
|
||||
created_at VARCHAR(30) NOT NULL DEFAULT '',
|
||||
updated_at VARCHAR(30) NOT NULL DEFAULT ''
|
||||
)""")
|
||||
conn.commit()
|
||||
|
||||
_exec(conn, """CREATE TABLE IF NOT EXISTS follow_up_records (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
target_type VARCHAR(1000) NOT NULL,
|
||||
target_id INT NOT NULL,
|
||||
followed_at VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
follower VARCHAR(1000) NOT NULL DEFAULT '慰心',
|
||||
follow_up_method VARCHAR(1000) NOT NULL DEFAULT '记录',
|
||||
content VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
next_action VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
next_follow_up_at VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
created_at VARCHAR(30) NOT NULL DEFAULT '',
|
||||
updated_at VARCHAR(30) NOT NULL DEFAULT ''
|
||||
)""")
|
||||
conn.commit()
|
||||
|
||||
_exec(conn, """CREATE TABLE IF NOT EXISTS business_proposals (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
customer_or_project_name VARCHAR(1000) NOT NULL,
|
||||
version VARCHAR(1000) NOT NULL,
|
||||
description VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
status VARCHAR(1000) NOT NULL DEFAULT '草稿',
|
||||
created_date VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
created_at VARCHAR(30) NOT NULL DEFAULT '',
|
||||
updated_at VARCHAR(30) NOT NULL DEFAULT ''
|
||||
)""")
|
||||
conn.commit()
|
||||
|
||||
_exec(conn, """CREATE TABLE IF NOT EXISTS operation_projects (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
project_name VARCHAR(1000) NOT NULL,
|
||||
project_version VARCHAR(1000) NOT NULL DEFAULT 'v1.0',
|
||||
project_type VARCHAR(1000) NOT NULL DEFAULT 'opportunity',
|
||||
project_status VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
current_stage VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
owner VARCHAR(1000) NOT NULL DEFAULT '慰心',
|
||||
start_date VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
end_date VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
target_customer VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
customer_need VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
expected_contract_amount DOUBLE NOT NULL DEFAULT 0,
|
||||
expected_sign_date VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
sign_probability DOUBLE NOT NULL DEFAULT 0,
|
||||
next_action VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
related_business_proposal_id INTEGER,
|
||||
sop_file_id INTEGER,
|
||||
sop_stage TEXT NOT NULL DEFAULT '',
|
||||
execution_progress REAL NOT NULL DEFAULT 0,
|
||||
current_deliverable TEXT NOT NULL DEFAULT '',
|
||||
risks TEXT NOT NULL DEFAULT '',
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS product_versions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
product_name TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
version_goal TEXT NOT NULL DEFAULT '',
|
||||
feature_list TEXT NOT NULL DEFAULT '',
|
||||
launch_date TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT '规划中',
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS finance_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
month TEXT NOT NULL,
|
||||
project_name TEXT NOT NULL DEFAULT '科普(慰心斋)',
|
||||
record_type TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT '',
|
||||
amount REAL NOT NULL DEFAULT 0,
|
||||
occurred_date TEXT NOT NULL DEFAULT '',
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS file_assets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
module TEXT NOT NULL,
|
||||
owner_id INTEGER NOT NULL,
|
||||
owner_version TEXT NOT NULL DEFAULT '',
|
||||
file_category TEXT NOT NULL DEFAULT '',
|
||||
file_name TEXT NOT NULL,
|
||||
file_type TEXT NOT NULL DEFAULT '',
|
||||
sop_stage VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
execution_progress DOUBLE NOT NULL DEFAULT 0,
|
||||
current_deliverable VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
risks VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
notes VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
created_at VARCHAR(30) NOT NULL DEFAULT '',
|
||||
updated_at VARCHAR(30) NOT NULL DEFAULT ''
|
||||
)""")
|
||||
conn.commit()
|
||||
|
||||
_exec(conn, """CREATE TABLE IF NOT EXISTS product_versions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
product_name VARCHAR(1000) NOT NULL,
|
||||
version VARCHAR(1000) NOT NULL,
|
||||
version_goal VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
feature_list VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
launch_date VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
status VARCHAR(1000) NOT NULL DEFAULT '规划中',
|
||||
notes VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
created_at VARCHAR(30) NOT NULL DEFAULT '',
|
||||
updated_at VARCHAR(30) NOT NULL DEFAULT ''
|
||||
)""")
|
||||
conn.commit()
|
||||
|
||||
_exec(conn, """CREATE TABLE IF NOT EXISTS finance_records (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
month VARCHAR(1000) NOT NULL,
|
||||
project_name VARCHAR(1000) NOT NULL DEFAULT '科普(慰心斋)',
|
||||
record_type VARCHAR(1000) NOT NULL,
|
||||
category VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
amount DOUBLE NOT NULL DEFAULT 0,
|
||||
occurred_date VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
notes VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
created_at VARCHAR(30) NOT NULL DEFAULT '',
|
||||
updated_at VARCHAR(30) NOT NULL DEFAULT ''
|
||||
)""")
|
||||
conn.commit()
|
||||
|
||||
_exec(conn, """CREATE TABLE IF NOT EXISTS file_assets (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
module VARCHAR(1000) NOT NULL,
|
||||
owner_id INT NOT NULL,
|
||||
owner_version VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
file_category VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
file_name VARCHAR(1000) NOT NULL,
|
||||
file_type VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
file_size INTEGER NOT NULL DEFAULT 0,
|
||||
file_path TEXT NOT NULL,
|
||||
file_path VARCHAR(1000) NOT NULL,
|
||||
is_external INTEGER NOT NULL DEFAULT 0,
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS project_tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
notes VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
created_at VARCHAR(30) NOT NULL DEFAULT '',
|
||||
updated_at VARCHAR(30) NOT NULL DEFAULT ''
|
||||
)""")
|
||||
conn.commit()
|
||||
|
||||
_exec(conn, """CREATE TABLE IF NOT EXISTS project_tasks (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
project_id INTEGER NOT NULL,
|
||||
phase TEXT NOT NULL DEFAULT '',
|
||||
milestone TEXT NOT NULL DEFAULT '',
|
||||
task TEXT NOT NULL DEFAULT '',
|
||||
owner TEXT NOT NULL DEFAULT '',
|
||||
due_date TEXT NOT NULL DEFAULT '',
|
||||
blockers TEXT NOT NULL DEFAULT '',
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
"""
|
||||
)
|
||||
# Schema migrations
|
||||
try: conn.execute("ALTER TABLE product_versions ADD COLUMN platform TEXT NOT NULL DEFAULT ''")
|
||||
phase VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
milestone VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
task VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
owner VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
due_date VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
blockers VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
notes VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
created_at VARCHAR(30) NOT NULL DEFAULT '',
|
||||
updated_at VARCHAR(30) NOT NULL DEFAULT ''
|
||||
)""")
|
||||
conn.commit()
|
||||
|
||||
# 用户表
|
||||
try: _exec(conn, """CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
display_name VARCHAR(100) NOT NULL,
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'opc_owner',
|
||||
created_at VARCHAR(30) NOT NULL DEFAULT ''
|
||||
)""")
|
||||
except: pass
|
||||
conn.commit()
|
||||
|
||||
# 用户-工作台关联表
|
||||
try: _exec(conn, """CREATE TABLE IF NOT EXISTS user_tenants (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
tenant VARCHAR(100) NOT NULL,
|
||||
UNIQUE KEY (user_id, tenant)
|
||||
)""")
|
||||
except: pass
|
||||
conn.commit()
|
||||
|
||||
# project_finances 表(月度预算 + 签约信息)
|
||||
try: _exec(conn, """CREATE TABLE IF NOT EXISTS project_finances (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
tenant VARCHAR(100) NOT NULL DEFAULT '科普·无界',
|
||||
project_id VARCHAR(100) NOT NULL DEFAULT '',
|
||||
business_type VARCHAR(100) NOT NULL DEFAULT '',
|
||||
customer_name VARCHAR(200) NOT NULL DEFAULT '',
|
||||
sign_amount DOUBLE NOT NULL DEFAULT 0,
|
||||
sign_month VARCHAR(20) NOT NULL DEFAULT '',
|
||||
status VARCHAR(50) NOT NULL DEFAULT '待签约',
|
||||
sales_person VARCHAR(100) NOT NULL DEFAULT '',
|
||||
total_rev DOUBLE NOT NULL DEFAULT 0,
|
||||
total_gross DOUBLE NOT NULL DEFAULT 0,
|
||||
budget_data TEXT,
|
||||
created_at VARCHAR(30) NOT NULL DEFAULT '',
|
||||
updated_at VARCHAR(30) NOT NULL DEFAULT ''
|
||||
)""")
|
||||
except: pass
|
||||
conn.commit()
|
||||
|
||||
# Schema migrations — 添加后续迁移的列(幂等)
|
||||
migrations = [
|
||||
"ALTER TABLE sales_leads ADD COLUMN tenant VARCHAR(100) NOT NULL DEFAULT '科普·无界'",
|
||||
"ALTER TABLE follow_up_records ADD COLUMN tenant VARCHAR(100) NOT NULL DEFAULT '科普·无界'",
|
||||
"ALTER TABLE business_proposals ADD COLUMN tenant VARCHAR(100) NOT NULL DEFAULT '科普·无界'",
|
||||
"ALTER TABLE business_proposals ADD COLUMN proposal_type VARCHAR(100) NOT NULL DEFAULT '业务方案'",
|
||||
"ALTER TABLE business_proposals ADD COLUMN notes VARCHAR(2000) NOT NULL DEFAULT ''",
|
||||
"ALTER TABLE operation_projects ADD COLUMN tenant VARCHAR(100) NOT NULL DEFAULT '科普·无界'",
|
||||
"ALTER TABLE product_versions ADD COLUMN tenant VARCHAR(100) NOT NULL DEFAULT '科普·无界'",
|
||||
"ALTER TABLE product_versions ADD COLUMN platform VARCHAR(100) NOT NULL DEFAULT ''",
|
||||
"ALTER TABLE finance_records ADD COLUMN tenant VARCHAR(100) NOT NULL DEFAULT '科普·无界'",
|
||||
"ALTER TABLE project_tasks ADD COLUMN tenant VARCHAR(100) NOT NULL DEFAULT '科普·无界'",
|
||||
"ALTER TABLE project_tasks ADD COLUMN status VARCHAR(50) NOT NULL DEFAULT '未开始'",
|
||||
"ALTER TABLE project_tasks ADD COLUMN sort_order INT NOT NULL DEFAULT 0",
|
||||
"ALTER TABLE project_tasks ADD COLUMN priority VARCHAR(10) NOT NULL DEFAULT 'P2'",
|
||||
# 12 月字段(确收/毛利/回款/费用/月度现金流)
|
||||
]
|
||||
for m in ["01","02","03","04","05","06","07","08","09","10","11","12"]:
|
||||
migrations.append(f"ALTER TABLE project_finances ADD COLUMN rev_2026_{m} DOUBLE NOT NULL DEFAULT 0")
|
||||
migrations.append(f"ALTER TABLE project_finances ADD COLUMN gross_2026_{m} DOUBLE NOT NULL DEFAULT 0")
|
||||
migrations.append(f"ALTER TABLE project_finances ADD COLUMN payment_2026_{m} DOUBLE NOT NULL DEFAULT 0")
|
||||
migrations.append(f"ALTER TABLE project_finances ADD COLUMN cost_2026_{m} DOUBLE NOT NULL DEFAULT 0")
|
||||
|
||||
for mig in migrations:
|
||||
try: _exec(conn, mig)
|
||||
except: pass
|
||||
conn.commit()
|
||||
|
||||
# 初始化默认用户(只执行一次)
|
||||
if not one(conn, "SELECT id FROM users LIMIT 1"):
|
||||
_exec(conn, """INSERT INTO users (username, password_hash, display_name, role, created_at) VALUES (?,?,?,?,?)""",
|
||||
("qiukai", generate_password_hash("yxcowork2026", "pbkdf2:sha256"), "qiukai", "admin", date.today().isoformat()))
|
||||
_exec(conn, """INSERT INTO users (username, password_hash, display_name, role, created_at) VALUES (?,?,?,?,?)""",
|
||||
("kepu", generate_password_hash("kepu123", "pbkdf2:sha256"), "科普负责人", "opc_owner", date.today().isoformat()))
|
||||
_exec(conn, """INSERT INTO users (username, password_hash, display_name, role, created_at) VALUES (?,?,?,?,?)""",
|
||||
("keyan", generate_password_hash("keyan123", "pbkdf2:sha256"), "科研负责人", "opc_owner", date.today().isoformat()))
|
||||
_exec(conn, """INSERT INTO users (username, password_hash, display_name, role, created_at) VALUES (?,?,?,?,?)""",
|
||||
("yihuan", generate_password_hash("yihuan123", "pbkdf2:sha256"), "医患负责人", "opc_owner", date.today().isoformat()))
|
||||
# 各 OPC 负责人绑定工作台
|
||||
for uname, tenant in [("kepu","科普·无界"),("keyan","科研·无界"),("yihuan","医患·无界")]:
|
||||
u = one(conn, "SELECT id FROM users WHERE username=?", (uname,))
|
||||
if u:
|
||||
_exec(conn, "INSERT INTO user_tenants (user_id, tenant) VALUES (?,?)", (u["id"], tenant))
|
||||
conn.commit()
|
||||
|
||||
if one(conn, "SELECT id FROM sales_leads LIMIT 1"):
|
||||
conn.close()
|
||||
@@ -173,16 +353,16 @@ CREATE TABLE IF NOT EXISTS project_tasks (
|
||||
("天广实生物", "P1", "待跟进", "血液肿瘤医生教育机会。"),
|
||||
]
|
||||
for customer, priority, status, note in sales:
|
||||
cur = conn.execute(
|
||||
cur = _exec(conn,
|
||||
"INSERT INTO sales_leads (target_customer, priority, status) VALUES (?,?,?)",
|
||||
(customer, priority, status),
|
||||
)
|
||||
conn.execute(
|
||||
_exec(conn,
|
||||
"INSERT INTO follow_up_records (target_type,target_id,followed_at,content,next_action) VALUES (?,?,?,?,?)",
|
||||
("sales", cur.lastrowid, date.today().isoformat(), note, "明确下一次沟通人和时间"),
|
||||
)
|
||||
|
||||
cur = conn.execute(
|
||||
cur = _exec(conn,
|
||||
"INSERT INTO business_proposals (customer_or_project_name,version,description,status,created_date) VALUES (?,?,?,?,?)",
|
||||
("信达生物", "v1.5", "信达科普项目续约与报价方案", "已提交客户", "2026-05-28"),
|
||||
)
|
||||
@@ -204,14 +384,14 @@ CREATE TABLE IF NOT EXISTS project_tasks (
|
||||
]
|
||||
op_dir = WEIXIN_BASE / "3、运营方案"
|
||||
for name, version, kind, status, stage, progress, note in projects:
|
||||
cur = conn.execute(
|
||||
cur = _exec(conn,
|
||||
"""INSERT INTO operation_projects
|
||||
(project_name,project_version,project_type,project_status,current_stage,target_customer,customer_need,
|
||||
expected_contract_amount,expected_sign_date,sign_probability,next_action,sop_stage,execution_progress,current_deliverable)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(name, version, kind, status, stage, "圆心科技", "科普内容项目执行与管理", 0 if kind == "execution" else 200, "2026-06", 100 if kind == "execution" else 70, "补齐版本要求文件并更新下一节点", stage, progress, note),
|
||||
)
|
||||
conn.execute(
|
||||
_exec(conn,
|
||||
"INSERT INTO follow_up_records (target_type,target_id,followed_at,content,next_action) VALUES (?,?,?,?,?)",
|
||||
("operation", cur.lastrowid, date.today().isoformat(), note, "补齐版本要求文件并更新下一节点"),
|
||||
)
|
||||
@@ -234,11 +414,11 @@ CREATE TABLE IF NOT EXISTS project_tasks (
|
||||
("渠道分发引擎", "v1.0", "六渠道统一分发", "分发 API、内容适配、分发排期、效果追踪", "2027-Q1", "规划中", "科普平台"),
|
||||
]
|
||||
for product in products:
|
||||
cur = conn.execute(
|
||||
cur = _exec(conn,
|
||||
"INSERT INTO product_versions (product_name,version,version_goal,feature_list,launch_date,status,platform) VALUES (?,?,?,?,?,?,?)",
|
||||
product,
|
||||
)
|
||||
conn.execute(
|
||||
_exec(conn,
|
||||
"INSERT INTO follow_up_records (target_type,target_id,followed_at,content,next_action) VALUES (?,?,?,?,?)",
|
||||
("product", cur.lastrowid, date.today().isoformat(), f"{product[0]} {product[1]}:{product[2]}", "按路线图推进"),
|
||||
)
|
||||
@@ -250,7 +430,7 @@ CREATE TABLE IF NOT EXISTS project_tasks (
|
||||
("2026-05", "cost_expense", "运营管理", 16, "项目管理与渠道协同"),
|
||||
("2026-06", "cost_expense", "渠道分发", 24, "投放与分发费用"),
|
||||
]:
|
||||
conn.execute(
|
||||
_exec(conn,
|
||||
"INSERT INTO finance_records (month,record_type,category,amount,occurred_date,notes) VALUES (?,?,?,?,?,?)",
|
||||
(month, record_type, category, amount, f"{month}-01", notes),
|
||||
)
|
||||
@@ -269,7 +449,7 @@ CREATE TABLE IF NOT EXISTS project_tasks (
|
||||
("阶段2:系统与标准搭建", "脚本生产及审核", "生产脚本", "军营", "2026-06-12", "脚本目前生产比较机械,需要提前准备", "细分标签领域完成"),
|
||||
]
|
||||
for phase, milestone, task, owner, due_date, blockers, notes in tasks_seed:
|
||||
conn.execute(
|
||||
_exec(conn,
|
||||
"INSERT INTO project_tasks (project_id,phase,milestone,task,owner,due_date,blockers,notes) VALUES (?,?,?,?,?,?,?,?)",
|
||||
(1, phase, milestone, task, owner, due_date, blockers, notes),
|
||||
)
|
||||
@@ -282,7 +462,7 @@ def add_file_index(conn, module, owner_id, owner_version, category, path, extern
|
||||
path = Path(path)
|
||||
if not path.exists():
|
||||
return
|
||||
conn.execute(
|
||||
_exec(conn,
|
||||
"""INSERT INTO file_assets
|
||||
(module,owner_id,owner_version,file_category,file_name,file_type,file_size,file_path,is_external)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)""",
|
||||
@@ -348,12 +528,20 @@ def monthly_finance(conn, tenant="科普·无界"):
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
if "user_id" not in session:
|
||||
return redirect("/login")
|
||||
return render_template("index.html")
|
||||
|
||||
|
||||
@app.route("/api/bootstrap")
|
||||
def bootstrap():
|
||||
tenant = request.args.get("tenant", "科普·无界")
|
||||
if "user_id" not in session:
|
||||
return jsonify({"error": "未登录"}), 401
|
||||
tenant = request.args.get("tenant", session.get("tenants", ["科普·无界"])[0])
|
||||
# 验证用户是否有权限访问该 workbench
|
||||
allowed = session.get("tenants", [])
|
||||
if tenant not in allowed:
|
||||
tenant = allowed[0]
|
||||
conn = db()
|
||||
try:
|
||||
def q(sql, *args):
|
||||
@@ -419,23 +607,24 @@ def bootstrap():
|
||||
"recent": q("SELECT * FROM follow_up_records WHERE tenant=? ORDER BY id DESC LIMIT 8", tenant),
|
||||
"risks": [{"title": "执行提醒", "content": x["next_action"]} for x in operations if x["next_action"]][:5],
|
||||
}
|
||||
return jsonify({"summary": summary, "sales": sales, "proposals": proposals, "operations": operations, "products": products, "finance": finance, "projectFinances": pfs, "financeMonthly": monthly_finance(conn, tenant), "tasks": tasks, "tenant": tenant, "tenants": ["科普·无界","科研·无界","医患·无界"]})
|
||||
return jsonify({"summary": summary, "sales": sales, "proposals": proposals, "operations": operations, "products": products, "finance": finance, "projectFinances": pfs, "financeMonthly": monthly_finance(conn, tenant), "tasks": tasks, "tenant": tenant, "tenants": allowed})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
TABLES = {
|
||||
"sales": ("sales_leads", ["target_customer", "priority", "status", "tenant"]),
|
||||
"proposals": ("business_proposals", ["customer_or_project_name", "version", "description", "status", "created_date", "tenant"]),
|
||||
"proposals": ("business_proposals", ["customer_or_project_name", "version", "description", "status", "created_date", "proposal_type", "notes", "tenant"]),
|
||||
"operations": ("operation_projects", ["project_name", "project_version", "project_type", "project_status", "current_stage", "owner", "target_customer", "customer_need", "expected_contract_amount", "expected_sign_date", "sign_probability", "next_action", "sop_stage", "execution_progress", "current_deliverable", "risks", "notes", "tenant"]),
|
||||
"products": ("product_versions", ["product_name", "version", "version_goal", "feature_list", "launch_date", "status", "platform", "notes", "tenant"]),
|
||||
"finance": ("finance_records", ["month", "project_name", "record_type", "category", "amount", "occurred_date", "notes", "tenant"]),
|
||||
"tasks": ("project_tasks", ["project_id", "phase", "milestone", "task", "owner", "due_date", "blockers", "notes", "status", "sort_order", "tenant"]),
|
||||
"projectFinances": ("project_finances", ["project_id", "tenant", "business_type", "customer_name", "sign_amount", "sign_month", "status", "sales_person", "rev_2026_06", "rev_2026_07", "rev_2026_08", "rev_2026_09", "gross_2026_06", "gross_2026_07", "gross_2026_08", "gross_2026_09"]),
|
||||
"tasks": ("project_tasks", ["project_id", "phase", "milestone", "task", "owner", "due_date", "blockers", "notes", "status", "sort_order", "priority", "tenant"]),
|
||||
"projectFinances": ("project_finances", ["project_id", "tenant", "business_type", "customer_name", "sign_amount", "sign_month", "status", "sales_person", "total_rev", "total_gross", "budget_data"]),
|
||||
}
|
||||
|
||||
|
||||
@app.route("/api/<resource>", methods=["POST"])
|
||||
@login_required
|
||||
def create_resource(resource):
|
||||
if resource not in TABLES:
|
||||
return jsonify({"error": "unknown resource"}), 404
|
||||
@@ -444,7 +633,7 @@ def create_resource(resource):
|
||||
values = [payload.get(col, "") for col in cols]
|
||||
conn = db()
|
||||
try:
|
||||
cur = conn.execute(f"INSERT INTO {table} ({','.join(cols)}) VALUES ({','.join(['?'] * len(cols))})", values)
|
||||
cur = _exec(conn, f"INSERT INTO {table} ({','.join(cols)}) VALUES ({','.join(['?'] * len(cols))})", values)
|
||||
conn.commit()
|
||||
return jsonify({"id": cur.lastrowid})
|
||||
finally:
|
||||
@@ -452,6 +641,7 @@ def create_resource(resource):
|
||||
|
||||
|
||||
@app.route("/api/<resource>/<int:item_id>", methods=["PUT", "DELETE"])
|
||||
@login_required
|
||||
def update_resource(resource, item_id):
|
||||
if resource not in TABLES:
|
||||
return jsonify({"error": "unknown resource"}), 404
|
||||
@@ -459,13 +649,13 @@ def update_resource(resource, item_id):
|
||||
conn = db()
|
||||
try:
|
||||
if request.method == "DELETE":
|
||||
conn.execute(f"DELETE FROM {table} WHERE id=?", (item_id,))
|
||||
_exec(conn, f"DELETE FROM {table} WHERE id=?", (item_id,))
|
||||
conn.commit()
|
||||
return jsonify({"ok": True})
|
||||
payload = request.get_json(force=True).get("data", {})
|
||||
update_cols = [col for col in cols if col in payload]
|
||||
if update_cols:
|
||||
conn.execute(
|
||||
_exec(conn,
|
||||
f"UPDATE {table} SET {','.join([col + '=?' for col in update_cols])}, updated_at=? WHERE id=?",
|
||||
[payload[col] for col in update_cols] + [now(), item_id],
|
||||
)
|
||||
@@ -476,14 +666,15 @@ def update_resource(resource, item_id):
|
||||
|
||||
|
||||
@app.route("/api/followups/<target_type>/<int:target_id>", methods=["POST"])
|
||||
@login_required
|
||||
def add_followup(target_type, target_id):
|
||||
payload = request.get_json(force=True).get("data", {})
|
||||
conn = db()
|
||||
try:
|
||||
conn.execute(
|
||||
_exec(conn,
|
||||
"""INSERT INTO follow_up_records
|
||||
(target_type,target_id,followed_at,follower,follow_up_method,content,next_action,next_follow_up_at)
|
||||
VALUES (?,?,?,?,?,?,?,?)""",
|
||||
(target_type,target_id,followed_at,follower,follow_up_method,content,next_action,next_follow_up_at,tenant)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
target_type,
|
||||
target_id,
|
||||
@@ -493,6 +684,7 @@ def add_followup(target_type, target_id):
|
||||
payload.get("content") or "",
|
||||
payload.get("next_action") or "",
|
||||
payload.get("next_follow_up_at") or "",
|
||||
payload.get("tenant") or "科普·无界",
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
@@ -502,10 +694,11 @@ def add_followup(target_type, target_id):
|
||||
|
||||
|
||||
@app.route("/api/followups/<int:followup_id>", methods=["DELETE"])
|
||||
@login_required
|
||||
def delete_followup(followup_id):
|
||||
conn = db()
|
||||
try:
|
||||
cur = conn.execute("DELETE FROM follow_up_records WHERE id=?", (followup_id,))
|
||||
cur = _exec(conn, "DELETE FROM follow_up_records WHERE id=?", (followup_id,))
|
||||
conn.commit()
|
||||
if cur.rowcount == 0:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
@@ -515,12 +708,13 @@ def delete_followup(followup_id):
|
||||
|
||||
|
||||
@app.route("/api/tasks/batch-sort", methods=["POST"])
|
||||
@login_required
|
||||
def batch_sort_tasks():
|
||||
conn = db()
|
||||
try:
|
||||
items = request.get_json(force=True).get("items", [])
|
||||
for item in items:
|
||||
conn.execute("UPDATE project_tasks SET sort_order=? WHERE id=?", (item["sort_order"], item["id"]))
|
||||
_exec(conn, "UPDATE project_tasks SET sort_order=? WHERE id=?", (item["sort_order"], item["id"]))
|
||||
conn.commit()
|
||||
return jsonify({"ok": True})
|
||||
finally:
|
||||
@@ -528,6 +722,7 @@ def batch_sort_tasks():
|
||||
|
||||
|
||||
@app.route("/api/files/upload", methods=["POST"])
|
||||
@login_required
|
||||
def upload_file():
|
||||
file = request.files["file"]
|
||||
module = request.form["module"]
|
||||
@@ -563,6 +758,7 @@ def file_content(file_id):
|
||||
|
||||
|
||||
@app.route("/api/files/<int:file_id>", methods=["DELETE"])
|
||||
@login_required
|
||||
def delete_file(file_id):
|
||||
conn = db()
|
||||
try:
|
||||
@@ -573,7 +769,7 @@ def delete_file(file_id):
|
||||
path = Path(asset["file_path"])
|
||||
if path.exists() and str(UPLOAD_DIR) in str(path.resolve()):
|
||||
path.unlink(missing_ok=True)
|
||||
conn.execute("DELETE FROM file_assets WHERE id=?", (file_id,))
|
||||
_exec(conn, "DELETE FROM file_assets WHERE id=?", (file_id,))
|
||||
conn.commit()
|
||||
return jsonify({"ok": True})
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user