From fd0c9f4aeb0a17e5414f843ca63dc8ff57bbda9d Mon Sep 17 00:00:00 2001 From: yumoqing Date: Fri, 8 May 2026 14:39:46 +0800 Subject: [PATCH] fix: CRUD JSON compliance - delete non-CRUD files, add editable to missing files - DELETED 3 non-CRUD format files: funnel_analysis.json, revenue_prediction.json, stage_change.json (these used custom name/title/type/components format instead of tblname/params CRUD spec) - ADDED editable paragraph to sales_stages.json (was missing new/update/delete data URLs) - ADDED editable paragraph to opportunity_stage_history.json (was missing new/update/delete data URLs) - CREATED 6 CRUD API files: sales_stages_create/update/delete.dspy, opportunity_stage_history_create/update/delete.dspy --- json/funnel_analysis.json | 82 -------------- json/opportunity_stage_history.json | 23 +++- json/revenue_prediction.json | 103 ------------------ json/sales_stages.json | 41 +++++-- json/stage_change.json | 44 -------- .../api/opportunity_stage_history_create.dspy | 38 +++++++ .../api/opportunity_stage_history_delete.dspy | 23 ++++ .../api/opportunity_stage_history_update.dspy | 36 ++++++ wwwroot/api/sales_stages_create.dspy | 38 +++++++ wwwroot/api/sales_stages_delete.dspy | 23 ++++ wwwroot/api/sales_stages_update.dspy | 41 +++++++ 11 files changed, 253 insertions(+), 239 deletions(-) delete mode 100644 json/funnel_analysis.json delete mode 100644 json/revenue_prediction.json delete mode 100644 json/stage_change.json create mode 100644 wwwroot/api/opportunity_stage_history_create.dspy create mode 100644 wwwroot/api/opportunity_stage_history_delete.dspy create mode 100644 wwwroot/api/opportunity_stage_history_update.dspy create mode 100644 wwwroot/api/sales_stages_create.dspy create mode 100644 wwwroot/api/sales_stages_delete.dspy create mode 100644 wwwroot/api/sales_stages_update.dspy diff --git a/json/funnel_analysis.json b/json/funnel_analysis.json deleted file mode 100644 index ec519ea..0000000 --- a/json/funnel_analysis.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "name": "funnel_analysis", - "title": "销售漏斗分析", - "type": "page", - "components": [ - { - "type": "container", - "name": "filters", - "title": "筛选条件", - "components": [ - { - "type": "select", - "name": "region", - "label": "区域", - "options": [] - }, - { - "type": "select", - "name": "owner_id", - "label": "销售负责人", - "options": [] - }, - { - "type": "date_range", - "name": "date_range", - "label": "时间范围" - }, - { - "type": "button", - "name": "search", - "label": "查询", - "action": "refresh_funnel_data" - } - ] - }, - { - "type": "chart", - "name": "funnel_chart", - "title": "销售漏斗图", - "chart_type": "funnel", - "data_source": "funnel_analysis_api", - "config": { - "value_field": "total_amount", - "category_field": "sales_stage", - "show_percentage": true - } - }, - { - "type": "chart", - "name": "amount_pie", - "title": "各阶段金额占比", - "chart_type": "pie", - "data_source": "funnel_analysis_api", - "config": { - "value_field": "total_amount", - "category_field": "sales_stage" - } - }, - { - "type": "card", - "name": "summary_stats", - "title": "汇总统计", - "components": [ - { - "type": "statistic", - "name": "total_opportunities", - "label": "商机总数" - }, - { - "type": "statistic", - "name": "total_amount", - "label": "预估总金额" - }, - { - "type": "statistic", - "name": "predicted_revenue", - "label": "预测成交金额" - } - ] - } - ] -} \ No newline at end of file diff --git a/json/opportunity_stage_history.json b/json/opportunity_stage_history.json index 507bc8e..e3f1583 100644 --- a/json/opportunity_stage_history.json +++ b/json/opportunity_stage_history.json @@ -2,13 +2,30 @@ "tblname": "opportunity_stage_history", "title": "阶段变更历史", "params": { - "sortby": ["changed_at desc"], + "sortby": [ + "changed_at desc" + ], "logined_userid": "changed_by_id", "confidential_fields": [], "browserfields": { - "exclouded": ["id", "opportunity_id", "changed_by_id", "changed_at"], + "exclouded": [ + "id", + "opportunity_id", + "changed_by_id", + "changed_at" + ], "alters": {} }, - "editexclouded": ["id", "opportunity_id", "changed_by_id", "changed_at"] + "editexclouded": [ + "id", + "opportunity_id", + "changed_by_id", + "changed_at" + ], + "editable": { + "new_data_url": "{{entire_url('../api/opportunity_stage_history_create.dspy')}}", + "update_data_url": "{{entire_url('../api/opportunity_stage_history_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/opportunity_stage_history_delete.dspy')}}" + } } } \ No newline at end of file diff --git a/json/revenue_prediction.json b/json/revenue_prediction.json deleted file mode 100644 index 1c7ebfb..0000000 --- a/json/revenue_prediction.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "name": "revenue_prediction", - "title": "收入预测", - "type": "page", - "components": [ - { - "type": "container", - "name": "filters", - "title": "预测条件", - "components": [ - { - "type": "select", - "name": "owner_id", - "label": "销售负责人", - "options": [] - }, - { - "type": "select", - "name": "region", - "label": "区域", - "options": [] - }, - { - "type": "select", - "name": "period", - "label": "历史数据周期", - "options": [ - {"value": "last_3_months", "text": "最近3个月"}, - {"value": "last_6_months", "text": "最近6个月"}, - {"value": "last_year", "text": "最近1年"} - ] - }, - { - "type": "button", - "name": "predict", - "label": "生成预测", - "action": "run_prediction" - } - ] - }, - { - "type": "card", - "name": "prediction_results", - "title": "预测结果", - "components": [ - { - "type": "statistic", - "name": "final_prediction", - "label": "最终预测收入", - "format": "currency" - }, - { - "type": "statistic", - "name": "stage_based_prediction", - "label": "阶段概率预测", - "format": "currency" - }, - { - "type": "statistic", - "name": "historical_based_prediction", - "label": "历史转化率预测", - "format": "currency" - }, - { - "type": "progress", - "name": "confidence_level", - "label": "预测置信度", - "value_field": "confidence_level", - "config": { - "high": 90, - "medium": 70, - "low": 50 - } - } - ] - }, - { - "type": "chart", - "name": "historical_conversion", - "title": "历史转化率趋势", - "chart_type": "line", - "data_source": "conversion_rate_analysis_api", - "config": { - "x_field": "month", - "y_field": "conversion_rate", - "show_markers": true - } - }, - { - "type": "table", - "name": "opportunity_details", - "title": "商机明细", - "data_source": "active_opportunities_api", - "columns": [ - {"field": "customer_name", "title": "客户名称"}, - {"field": "estimated_amount", "title": "预估金额", "format": "currency"}, - {"field": "sales_stage", "title": "销售阶段"}, - {"field": "probability", "title": "成交概率", "format": "percentage"}, - {"field": "expected_close_date", "title": "预计成交时间"} - ] - } - ] -} \ No newline at end of file diff --git a/json/sales_stages.json b/json/sales_stages.json index 609fcfc..edbebb2 100644 --- a/json/sales_stages.json +++ b/json/sales_stages.json @@ -2,27 +2,54 @@ "tblname": "sales_stages", "title": "销售阶段配置", "params": { - "sortby": ["stage_order asc"], + "sortby": [ + "stage_order asc" + ], "confidential_fields": [], "browserfields": { - "exclouded": ["id", "created_at", "updated_at"], + "exclouded": [ + "id", + "created_at", + "updated_at" + ], "alters": { "is_won_stage": { "uitype": "code", "data": [ - {"value": "yes", "text": "是"}, - {"value": "no", "text": "否"} + { + "value": "yes", + "text": "是" + }, + { + "value": "no", + "text": "否" + } ] }, "is_lost_stage": { "uitype": "code", "data": [ - {"value": "yes", "text": "是"}, - {"value": "no", "text": "否"} + { + "value": "yes", + "text": "是" + }, + { + "value": "no", + "text": "否" + } ] } } }, - "editexclouded": ["id", "created_at", "updated_at"] + "editexclouded": [ + "id", + "created_at", + "updated_at" + ], + "editable": { + "new_data_url": "{{entire_url('../api/sales_stages_create.dspy')}}", + "update_data_url": "{{entire_url('../api/sales_stages_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/sales_stages_delete.dspy')}}" + } } } \ No newline at end of file diff --git a/json/stage_change.json b/json/stage_change.json deleted file mode 100644 index 5c24a1a..0000000 --- a/json/stage_change.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "stage_change", - "title": "商机阶段变更", - "type": "form", - "data_source": "opportunity_by_id_api", - "components": [ - { - "type": "display", - "name": "customer_name", - "label": "客户名称" - }, - { - "type": "display", - "name": "current_stage", - "label": "当前阶段" - }, - { - "type": "select", - "name": "new_stage", - "label": "新阶段", - "required": true, - "options": [ - {"value": "初步接洽", "text": "初步接洽"}, - {"value": "需求确认", "text": "需求确认"}, - {"value": "方案报价", "text": "方案报价"}, - {"value": "合同谈判", "text": "合同谈判"}, - {"value": "成交", "text": "成交"} - ] - }, - { - "type": "textarea", - "name": "change_reason", - "label": "变更原因", - "required": true, - "placeholder": "请输入阶段变更的原因..." - }, - { - "type": "button", - "name": "submit", - "label": "确认变更", - "action": "update_opportunity_stage" - } - ] -} \ No newline at end of file diff --git a/wwwroot/api/opportunity_stage_history_create.dspy b/wwwroot/api/opportunity_stage_history_create.dspy new file mode 100644 index 0000000..373fd87 --- /dev/null +++ b/wwwroot/api/opportunity_stage_history_create.dspy @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Opportunity stage history create API""" +import json, uuid, time + +result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}} + +try: + opportunity_id = params_kw.get('opportunity_id', '').strip() + old_stage = params_kw.get('old_stage', '').strip() + new_stage = params_kw.get('new_stage', '').strip() + changed_by_id = params_kw.get('changed_by_id', '').strip() + change_reason = params_kw.get('change_reason', '').strip() + + if not opportunity_id or not new_stage: + result['options'] = {'title': 'Error', 'message': '请填写必填字段', 'type': 'error'} + else: + dbname = get_module_dbname('opportunity_management') + new_id = str(uuid.uuid4()).replace('-', '') + now = time.strftime('%Y-%m-%d %H:%M:%S') + + async with DBPools().sqlorContext(dbname) as sor: + await sor.sqlExe("""INSERT INTO opportunity_stage_history (id, opportunity_id, old_stage, new_stage, changed_by_id, change_reason, changed_at) + VALUES (${id}$, ${opportunity_id}$, ${old_stage}$, ${new_stage}$, ${changed_by_id}$, ${change_reason}$, ${changed_at}$)""", { + 'id': new_id, + 'opportunity_id': opportunity_id, + 'old_stage': old_stage, + 'new_stage': new_stage, + 'changed_by_id': changed_by_id, + 'change_reason': change_reason, + 'changed_at': now + }) + result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '阶段历史记录创建成功', 'type': 'success'}} + +except Exception as e: + result['options'] = {'title': 'Error', 'message': f'创建失败: {str(e)}', 'type': 'error'} + +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/opportunity_stage_history_delete.dspy b/wwwroot/api/opportunity_stage_history_delete.dspy new file mode 100644 index 0000000..25302ff --- /dev/null +++ b/wwwroot/api/opportunity_stage_history_delete.dspy @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Opportunity stage history delete API""" +import json + +result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}} + +try: + row_id = params_kw.get('id', '').strip() + + if not row_id: + result['options'] = {'title': 'Error', 'message': '缺少记录ID', 'type': 'error'} + else: + dbname = get_module_dbname('opportunity_management') + + async with DBPools().sqlorContext(dbname) as sor: + await sor.sqlExe("DELETE FROM opportunity_stage_history WHERE id=${id}$", {'id': row_id}) + result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '阶段历史记录删除成功', 'type': 'success'}} + +except Exception as e: + result['options'] = {'title': 'Error', 'message': f'删除失败: {str(e)}', 'type': 'error'} + +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/opportunity_stage_history_update.dspy b/wwwroot/api/opportunity_stage_history_update.dspy new file mode 100644 index 0000000..19ab709 --- /dev/null +++ b/wwwroot/api/opportunity_stage_history_update.dspy @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Opportunity stage history update API""" +import json, time + +result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}} + +try: + row_id = params_kw.get('id', '').strip() + old_stage = params_kw.get('old_stage', '').strip() + new_stage = params_kw.get('new_stage', '').strip() + change_reason = params_kw.get('change_reason', '').strip() + + if not row_id: + result['options'] = {'title': 'Error', 'message': '缺少记录ID', 'type': 'error'} + else: + dbname = get_module_dbname('opportunity_management') + now = time.strftime('%Y-%m-%d %H:%M:%S') + + async with DBPools().sqlorContext(dbname) as sor: + await sor.sqlExe("""UPDATE opportunity_stage_history SET + old_stage=${old_stage}$, new_stage=${new_stage}$, + change_reason=${change_reason}$, changed_at=${changed_at}$ + WHERE id=${id}$""", { + 'id': row_id, + 'old_stage': old_stage, + 'new_stage': new_stage, + 'change_reason': change_reason, + 'changed_at': now + }) + result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '阶段历史记录更新成功', 'type': 'success'}} + +except Exception as e: + result['options'] = {'title': 'Error', 'message': f'更新失败: {str(e)}', 'type': 'error'} + +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/sales_stages_create.dspy b/wwwroot/api/sales_stages_create.dspy new file mode 100644 index 0000000..7df488d --- /dev/null +++ b/wwwroot/api/sales_stages_create.dspy @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Sales stages create API""" +import json, uuid, time + +result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}} + +try: + stage_name = params_kw.get('stage_name', '').strip() + stage_order = params_kw.get('stage_order', '0').strip() + conversion_rate = params_kw.get('conversion_rate', '0').strip() + is_won_stage = params_kw.get('is_won_stage', 'no').strip() + is_lost_stage = params_kw.get('is_lost_stage', 'no').strip() + + if not stage_name or not stage_order: + result['options'] = {'title': 'Error', 'message': '请填写必填字段', 'type': 'error'} + else: + dbname = get_module_dbname('opportunity_management') + new_id = str(uuid.uuid4()).replace('-', '') + now = time.strftime('%Y-%m-%d %H:%M:%S') + + async with DBPools().sqlorContext(dbname) as sor: + await sor.sqlExe("""INSERT INTO sales_stages (id, stage_name, stage_order, conversion_rate, is_won_stage, is_lost_stage, created_at, updated_at) + VALUES (${id}$, ${stage_name}$, ${stage_order}$, ${conversion_rate}$, ${is_won_stage}$, ${is_lost_stage}$, ${created_at}$, ${updated_at}$)""", { + 'id': new_id, + 'stage_name': stage_name, + 'stage_order': int(stage_order), + 'conversion_rate': float(conversion_rate) if conversion_rate else 0.0, + 'is_won_stage': is_won_stage, + 'is_lost_stage': is_lost_stage, + 'created_at': now, 'updated_at': now + }) + result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '销售阶段创建成功', 'type': 'success'}} + +except Exception as e: + result['options'] = {'title': 'Error', 'message': f'创建失败: {str(e)}', 'type': 'error'} + +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/sales_stages_delete.dspy b/wwwroot/api/sales_stages_delete.dspy new file mode 100644 index 0000000..87b2ef3 --- /dev/null +++ b/wwwroot/api/sales_stages_delete.dspy @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Sales stages delete API""" +import json + +result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}} + +try: + row_id = params_kw.get('id', '').strip() + + if not row_id: + result['options'] = {'title': 'Error', 'message': '缺少记录ID', 'type': 'error'} + else: + dbname = get_module_dbname('opportunity_management') + + async with DBPools().sqlorContext(dbname) as sor: + await sor.sqlExe("DELETE FROM sales_stages WHERE id=${id}$", {'id': row_id}) + result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '销售阶段删除成功', 'type': 'success'}} + +except Exception as e: + result['options'] = {'title': 'Error', 'message': f'删除失败: {str(e)}', 'type': 'error'} + +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/sales_stages_update.dspy b/wwwroot/api/sales_stages_update.dspy new file mode 100644 index 0000000..3f7b372 --- /dev/null +++ b/wwwroot/api/sales_stages_update.dspy @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Sales stages update API""" +import json, time + +result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}} + +try: + row_id = params_kw.get('id', '').strip() + stage_name = params_kw.get('stage_name', '').strip() + stage_order = params_kw.get('stage_order', '0').strip() + conversion_rate = params_kw.get('conversion_rate', '0').strip() + is_won_stage = params_kw.get('is_won_stage', 'no').strip() + is_lost_stage = params_kw.get('is_lost_stage', 'no').strip() + + if not row_id or not stage_name: + result['options'] = {'title': 'Error', 'message': '缺少必要参数', 'type': 'error'} + else: + dbname = get_module_dbname('opportunity_management') + now = time.strftime('%Y-%m-%d %H:%M:%S') + + async with DBPools().sqlorContext(dbname) as sor: + await sor.sqlExe("""UPDATE sales_stages SET + stage_name=${stage_name}$, stage_order=${stage_order}$, + conversion_rate=${conversion_rate}$, is_won_stage=${is_won_stage}$, + is_lost_stage=${is_lost_stage}$, updated_at=${updated_at}$ + WHERE id=${id}$""", { + 'id': row_id, + 'stage_name': stage_name, + 'stage_order': int(stage_order), + 'conversion_rate': float(conversion_rate) if conversion_rate else 0.0, + 'is_won_stage': is_won_stage, + 'is_lost_stage': is_lost_stage, + 'updated_at': now + }) + result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '销售阶段更新成功', 'type': 'success'}} + +except Exception as e: + result['options'] = {'title': 'Error', 'message': f'更新失败: {str(e)}', 'type': 'error'} + +return json.dumps(result, ensure_ascii=False)