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
This commit is contained in:
yumoqing 2026-05-08 14:39:46 +08:00
parent 635f71ea52
commit fd0c9f4aeb
11 changed files with 253 additions and 239 deletions

View File

@ -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": "预测成交金额"
}
]
}
]
}

View File

@ -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')}}"
}
}
}

View File

@ -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": "预计成交时间"}
]
}
]
}

View File

@ -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')}}"
}
}
}

View File

@ -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"
}
]
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)