diff --git a/backend/flask_app.py b/backend/flask_app.py index 443eaf7..8637b6f 100644 --- a/backend/flask_app.py +++ b/backend/flask_app.py @@ -856,7 +856,7 @@ TABLES = { "sales": ("sales_leads", ["target_customer", "priority", "status", "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"]), + "products": ("product_versions", ["product_name", "version", "version_goal", "priority", "start_date", "plan_date", "dev_done_date", "test_date", "launch_date", "status", "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", "priority", "tenant"]), "projectFinances": ("project_finances", ["project_id", "tenant", "business_type", "customer_name", "sign_amount", "sign_month", "status", "sales_person", "owner", "total_rev", "total_gross", "budget_data"]), @@ -913,6 +913,22 @@ def update_resource(resource, item_id): valid_statuses = ["未开始", "进行中", "已结束"] if not payload["status"] or payload["status"] not in valid_statuses: payload["status"] = "未开始" + # 产品日期约束:4 个时间不能早于启动时间;启动时间不能清空 + if resource == "products": + # 查当前记录的 start_date + cur = _exec(conn, f"SELECT start_date FROM {table} WHERE id=?", (item_id,)) + row = cur.fetchone() + cur.close() + current_start = (row or {}).get("start_date", "") or "" + new_start = payload.get("start_date", current_start) + # 启动时间必填 + if "start_date" in payload and not new_start: + return jsonify({"error": "启动时间为必填项"}), 400 + date_fields = ["plan_date", "dev_done_date", "test_date", "launch_date"] + for f in date_fields: + if f in payload and payload[f] and new_start and payload[f] < new_start: + labels = {"plan_date": "产品方案", "dev_done_date": "研发完成", "test_date": "测试完成", "launch_date": "上线时间"} + return jsonify({"error": f"{labels[f]}不能早于启动时间({new_start})"}), 400 update_cols = [col for col in cols if col in payload] if update_cols: _exec(conn, diff --git a/backend/migrations/__init__.py b/backend/migrations/__init__.py index 1996674..a62d639 100644 --- a/backend/migrations/__init__.py +++ b/backend/migrations/__init__.py @@ -17,12 +17,13 @@ def run_migrations(): """ from migrations.tables import migrate_create_tables from migrations.columns import migrate_add_columns - from migrations.data_fixes import migrate_fix_task_status, migrate_rename_tenant + from migrations.data_fixes import migrate_fix_task_status, migrate_rename_tenant, migrate_drop_product_fields from migrations.seed import migrate_seed_users, migrate_seed_demo_data migrate_create_tables() migrate_add_columns() migrate_fix_task_status() migrate_rename_tenant() + migrate_drop_product_fields() migrate_seed_users() migrate_seed_demo_data() diff --git a/backend/migrations/columns.py b/backend/migrations/columns.py index 81f6ef3..e11b67c 100644 --- a/backend/migrations/columns.py +++ b/backend/migrations/columns.py @@ -37,8 +37,20 @@ def migrate_add_columns(): "ALTER TABLE business_proposals ADD COLUMN notes VARCHAR(2000) NOT NULL DEFAULT ''") # product_versions 扩展字段 - _add_column_if_missing(conn, "product_versions", "platform", - "ALTER TABLE product_versions ADD COLUMN platform VARCHAR(100) NOT NULL DEFAULT ''") + _add_column_if_missing(conn, "product_versions", "priority", + "ALTER TABLE product_versions ADD COLUMN priority VARCHAR(10) NOT NULL DEFAULT 'P2'") + _add_column_if_missing(conn, "product_versions", "start_date", + "ALTER TABLE product_versions ADD COLUMN start_date VARCHAR(30) NOT NULL DEFAULT ''") + _add_column_if_missing(conn, "product_versions", "plan_date", + "ALTER TABLE product_versions ADD COLUMN plan_date VARCHAR(30) NOT NULL DEFAULT ''") + _add_column_if_missing(conn, "product_versions", "dev_done_date", + "ALTER TABLE product_versions ADD COLUMN dev_done_date VARCHAR(30) NOT NULL DEFAULT ''") + _add_column_if_missing(conn, "product_versions", "test_date", + "ALTER TABLE product_versions ADD COLUMN test_date VARCHAR(30) NOT NULL DEFAULT ''") + _add_column_if_missing(conn, "product_versions", "devs", + "ALTER TABLE product_versions ADD COLUMN devs VARCHAR(500) NOT NULL DEFAULT ''") + _add_column_if_missing(conn, "product_versions", "testers", + "ALTER TABLE product_versions ADD COLUMN testers VARCHAR(500) NOT NULL DEFAULT ''") # project_tasks 扩展字段 _add_column_if_missing(conn, "project_tasks", "status", diff --git a/backend/migrations/data_fixes.py b/backend/migrations/data_fixes.py index 4729f6d..56dd6c6 100644 --- a/backend/migrations/data_fixes.py +++ b/backend/migrations/data_fixes.py @@ -47,3 +47,27 @@ def migrate_rename_tenant(): conn.commit() finally: conn.close() + + +def migrate_drop_product_fields(): + """删除 product_versions 表的 owner / platform / feature_list 字段""" + from flask_app import db, mysql + + conn = db() + try: + for col in ["owner", "platform", "feature_list"]: + cur = conn.cursor(dictionary=True) + cur.execute("SHOW COLUMNS FROM product_versions LIKE %s", (col,)) + exists = cur.fetchone() + cur.close() + if exists: + try: + cur = conn.cursor() + cur.execute(f"ALTER TABLE product_versions DROP COLUMN {col}") + cur.close() + conn.commit() + print(f"[migrate] product_versions.{col} 列已删除") + except mysql.connector.Error as e: + print(f"[migrate] 删除 {col} 失败: {e}") + finally: + conn.close() diff --git a/static/modules/drawer.js b/static/modules/drawer.js index ca49b02..d0a5469 100644 --- a/static/modules/drawer.js +++ b/static/modules/drawer.js @@ -23,7 +23,7 @@ function openDrawer(resource, id) { ? [["project_name","项目名称"],["owner","负责人"],["expected_sign_date","截止时间"],["expected_contract_amount","金额"],["notes","项目说明"]] : resource === "proposals" ? [["customer_or_project_name","客户/项目"],["proposal_type","方案类型"],["notes","备注"]] - : [["product_name","产品名称"],["version","版本号"],["version_goal","版本目标"],["feature_list","核心功能"],["launch_date","上线日期"],["status","状态"],["notes","备注"]]; + : [["product_name","版本名称"],["version","版本号"],["priority","优先级"],["version_goal","版本目标"],["start_date","启动时间"],["plan_date","产品方案"],["dev_done_date","研发完成"],["test_date","测试完成"],["launch_date","上线时间"],["notes","进展备注"]]; const fieldIcons = { target_customer: "user", priority: "flag", status: "circle-dot", project_name: "briefcase-business", project_version: "git-branch", project_status: "circle-dot", current_stage: "map-pin", @@ -31,29 +31,38 @@ function openDrawer(resource, id) { sign_probability: "percent", sop_stage: "list-checks", execution_progress: "activity", current_deliverable: "package", risks: "alert-triangle", next_action: "arrow-right", product_name: "box", version: "tag", version_goal: "target", feature_list: "list", platform: "layers", - launch_date: "calendar", notes: "sticky-note", proposal_type: "tag", customer_or_project_name: "building" + launch_date: "calendar", notes: "sticky-note", proposal_type: "tag", customer_or_project_name: "building", + priority: "flag", owner: "user", start_date: "play", plan_date: "file-text", dev_done_date: "check-square", + test_date: "bug", devs: "users", testers: "shield-check" }; const multilineFields = ["customer_need", "current_deliverable", "risks", "next_action", "version_goal", "feature_list", "notes"]; const followupTarget = resource === "sales" ? "sales" : resource === "proposals" ? "proposal" : resource === "operations" ? "operation" : resource === "products" ? "product" : ""; const title = esc(item.target_customer || item.project_name || (item.customer_or_project_name ? `${item.customer_or_project_name} · ${item.proposal_type || ''}` : "") || item.product_name); const titleForAttr = esc(item.target_customer || item.project_name || (item.customer_or_project_name ? `${item.customer_or_project_name} · ${item.proposal_type || ''}` : "") || item.product_name); drawer.innerHTML = `
Detail Drawer
总耗时(上线−启动)
${dDays(item.start_date, item.launch_date)}
产品耗时(方案−启动)
${dDays(item.start_date, item.plan_date)}
研发耗时(研发完成−方案)
${dDays(item.plan_date, item.dev_done_date)}
测试耗时(测试完成−研发完成)
${dDays(item.dev_done_date, item.test_date)}
填写项目财务信息与月度预算