@dazitech/cli 3.0.7 → 3.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +1 -1
  2. package/dist/clis/dazi-app.js +1 -1
  3. package/dist/clis/dazi-flow.js +1 -1
  4. package/dist/clis/dazi-onto.js +73 -22
  5. package/dist/clis/dazi.js +266 -171
  6. package/dist/docs/flow/flow-project-guide.md +1 -1
  7. package/dist/docs/guides/quickstart.md +18 -4
  8. package/dist/docs/guides/troubleshooting.md +12 -1
  9. package/dist/docs/guides/workspace-v3.md +43 -23
  10. package/dist/docs/index.json +28 -3
  11. package/dist/docs/onto/action-guide.md +3 -3
  12. package/dist/docs/onto/dazi_script_sdk_reference.md +244 -174
  13. package/dist/docs/onto/dazi_script_seed_data_guide.md +158 -155
  14. package/dist/docs/onto/function-guide.md +82 -27
  15. package/dist/docs/onto/space-management.md +3 -1
  16. package/dist/docs/onto//346/234/254/344/275/223/345/210/206/347/261/273/350/247/204/345/210/222/344/270/216SDK/346/211/251/345/261/225/346/226/271/346/241/210.md +168 -0
  17. package/dist/docs/onto//346/234/254/344/275/223/345/221/275/345/220/215/350/247/204/350/214/203_/347/211/251/347/220/206/350/241/250Cube/344/270/216/345/257/271/350/261/241.md +402 -0
  18. package/dist/docs/onto//346/234/254/344/275/223/350/204/232/346/234/254/347/274/226/345/206/231/346/214/207/345/215/227.md +200 -34
  19. package/dist/docs/onto//346/234/254/344/275/223/350/247/204/345/210/222/346/214/207/345/215/227.md +188 -38
  20. package/dist/docs/onto//350/204/232/346/234/254/350/277/220/350/241/214/347/272/240/351/224/231_/345/225/206/345/212/241/346/210/220/346/234/254/346/226/271/346/241/210/345/274/200/345/217/221/350/277/207/347/250/213.md +213 -0
  21. package/dist/docs/onto//350/247/204/345/210/222/347/244/272/344/276/213_/344/272/247/345/223/201/351/224/200/345/224/256/346/234/254/344/275/223/350/247/204/345/210/222/346/226/271/346/241/210.md +620 -0
  22. package/dist/docs/onto//350/247/204/345/210/222/347/244/272/344/276/213_/345/210/251/346/266/246/345/210/206/346/236/220/346/234/254/344/275/223/346/226/271/346/241/210.md +680 -541
  23. package/dist/examples/index.json +208 -22
  24. package/dist/examples/onto/README.md +51 -0
  25. package/dist/examples/onto/_templates/ontology_function_template.py +50 -0
  26. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/functions/profit_fn_account_breakdown.py +62 -0
  27. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/functions/profit_fn_budget_vs_actual.py +69 -0
  28. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/functions/profit_fn_cost_center_profit.py +64 -0
  29. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/functions/profit_fn_get_summary.py +61 -0
  30. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/functions/profit_fn_mom_analysis.py +82 -0
  31. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/functions/profit_fn_top_accounts.py +61 -0
  32. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/functions/profit_fn_yoy_analysis.py +79 -0
  33. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/functions/save_test_arguments.ps1 +38 -0
  34. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/functions/test_arguments/profit.fn.account_breakdown.json +1 -0
  35. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/functions/test_arguments/profit.fn.budget_vs_actual.json +1 -0
  36. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/functions/test_arguments/profit.fn.cost_center_profit.json +1 -0
  37. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/functions/test_arguments/profit.fn.get_summary.json +1 -0
  38. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/functions/test_arguments/profit.fn.mom_analysis.json +1 -0
  39. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/functions/test_arguments/profit.fn.top_accounts.json +1 -0
  40. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/functions/test_arguments/profit.fn.yoy_analysis.json +1 -0
  41. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/setup/profit_ontology_init.py +679 -0
  42. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/setup/profit_seed_data.py +216 -0
  43. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/sales_fn_channel_mix.py +89 -0
  44. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/sales_fn_customer_segmentation.py +121 -0
  45. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/sales_fn_get_summary.py +78 -0
  46. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/sales_fn_mom_analysis.py +89 -0
  47. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/sales_fn_region_breakdown.py +84 -0
  48. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/sales_fn_top_products.py +98 -0
  49. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/sales_fn_yoy_analysis.py +87 -0
  50. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/save_test_arguments.ps1 +38 -0
  51. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/test_arguments/sales.fn.channel_mix.json +5 -0
  52. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/test_arguments/sales.fn.customer_segmentation.json +5 -0
  53. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/test_arguments/sales.fn.get_summary.json +5 -0
  54. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/test_arguments/sales.fn.mom_analysis.json +5 -0
  55. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/test_arguments/sales.fn.region_breakdown.json +5 -0
  56. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/test_arguments/sales.fn.top_products.json +5 -0
  57. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/test_arguments/sales.fn.yoy_analysis.json +5 -0
  58. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/setup/sales_ontology_init.py +539 -0
  59. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/setup/sales_seed_data.py +163 -0
  60. package/dist/prompts/index.json +2 -2
  61. package/dist/prompts/onto/action-design.md +4 -1
  62. package/dist/prompts/onto/function-design.md +46 -19
  63. package/dist/prompts/onto/rule-seed.md +5 -1
  64. package/dist/prompts/onto/script-publish-run.md +87 -25
  65. package/package.json +1 -1
  66. package/dist/examples/onto/function/profit_fn_customer_segmentation.py +0 -117
  67. package/dist/examples/onto/function/profit_fn_mom_analysis.py +0 -89
  68. package/dist/examples/onto/function/profit_fn_top_products.py +0 -89
  69. package/dist/examples/onto/function/profit_fn_yoy_analysis.py +0 -89
  70. package/dist/examples/onto/setup/profit_ontology_init.py +0 -388
@@ -0,0 +1,216 @@
1
+ """利润分析演示数据灌入 — space__misc_01
2
+
3
+ 前置:先执行 profit_ontology_init.py 建表。
4
+ 幂等:fact_gl_journal_entry 已有数据则跳过。
5
+
6
+ 放置:项目/DAZI_TEST/本体/ontos/利润分析示例/setup/profit_seed_data.py
7
+ 发布:dazi onto script publish 项目/DAZI_TEST/本体/ontos/利润分析示例/setup/profit_seed_data.py --space space__misc_01 --type data
8
+ """
9
+
10
+ import json
11
+ import random
12
+ from datetime import date, datetime, timedelta
13
+
14
+ _SEED_DT = datetime(2025, 1, 1, 0, 0, 0)
15
+ _BUDGET_VERSION = "2026年度预算"
16
+
17
+
18
+ def _calc_signed(account_type, debit, credit):
19
+ if account_type == "收入":
20
+ return round(credit - debit, 2)
21
+ if account_type in ("成本", "费用"):
22
+ return round(debit - credit, 2)
23
+ return round(debit - credit, 2)
24
+
25
+
26
+ def _make_entry_line(seq, posting_date, account, cc, amount):
27
+ account_type = account["account_type"]
28
+ debit = amount if account_type in ("成本", "费用") else 0.0
29
+ credit = amount if account_type == "收入" else 0.0
30
+ if account_type == "收入":
31
+ debit, credit = 0.0, amount
32
+ signed = _calc_signed(account_type, debit, credit)
33
+ fy = posting_date.year
34
+ fp = posting_date.month
35
+ entry_id = f"JE{posting_date.strftime('%Y%m%d')}{seq // 10:04d}"
36
+ line_id = f"JL{posting_date.strftime('%Y%m%d')}{seq:05d}"
37
+ date_key = int(posting_date.strftime("%Y%m%d"))
38
+ return {
39
+ "entry_id": entry_id,
40
+ "line_id": line_id,
41
+ "date_key": date_key,
42
+ "posting_date": posting_date,
43
+ "fiscal_year": fy,
44
+ "fiscal_period": fp,
45
+ "account_id": account["account_id"],
46
+ "account_code": account["account_code"],
47
+ "account_name": account["account_name"],
48
+ "account_type": account["account_type"],
49
+ "pl_category": account["pl_category"],
50
+ "account_level": account["account_level"],
51
+ "cost_center_id": cc["cost_center_id"],
52
+ "cost_center_name": cc["cost_center_name"],
53
+ "department": cc["department"],
54
+ "profit_center": cc["profit_center"],
55
+ "debit_amount": round(debit, 2),
56
+ "credit_amount": round(credit, 2),
57
+ "amount_signed": signed,
58
+ "currency": "CNY",
59
+ "voucher_no": f"V{posting_date.strftime('%Y%m')}{seq:04d}",
60
+ "source_system": "GL",
61
+ "description": f"{account['account_name']}-{cc['cost_center_name']}",
62
+ "created_at": datetime.combine(posting_date, datetime.min.time()),
63
+ }
64
+
65
+
66
+ def main():
67
+ space_id = "space__misc_01"
68
+ s = space.get(space_id)
69
+
70
+ output.print("=== 利润分析演示数据灌入 ===")
71
+
72
+ try:
73
+ n = int(s.sql.query_one("SELECT count() FROM fact_gl_journal_entry") or 0)
74
+ except Exception:
75
+ n = 0
76
+ if n > 0:
77
+ output.print(f"fact_gl_journal_entry 已有 {n} 行,跳过灌数")
78
+ output.print("__JSON_SUMMARY__" + json.dumps({"ok": True, "skipped": True, "rows": n}, ensure_ascii=True))
79
+ return
80
+
81
+ accounts = [
82
+ {"account_id": "ACC6000", "account_code": "6000", "account_name": "营业收入", "account_type": "收入", "pl_category": "营业收入", "parent_account_id": "", "account_level": 1, "is_leaf": False, "normal_balance": "贷", "status": "启用"},
83
+ {"account_id": "ACC6001", "account_code": "6001", "account_name": "主营业务收入", "account_type": "收入", "pl_category": "营业收入", "parent_account_id": "ACC6000", "account_level": 2, "is_leaf": True, "normal_balance": "贷", "status": "启用"},
84
+ {"account_id": "ACC6050", "account_code": "6050", "account_name": "其他业务收入", "account_type": "收入", "pl_category": "营业收入", "parent_account_id": "ACC6000", "account_level": 2, "is_leaf": True, "normal_balance": "贷", "status": "启用"},
85
+ {"account_id": "ACC6400", "account_code": "6400", "account_name": "营业成本", "account_type": "成本", "pl_category": "营业成本", "parent_account_id": "", "account_level": 1, "is_leaf": False, "normal_balance": "借", "status": "启用"},
86
+ {"account_id": "ACC6401", "account_code": "6401", "account_name": "主营业务成本", "account_type": "成本", "pl_category": "营业成本", "parent_account_id": "ACC6400", "account_level": 2, "is_leaf": True, "normal_balance": "借", "status": "启用"},
87
+ {"account_id": "ACC6402", "account_code": "6402", "account_name": "原材料成本", "account_type": "成本", "pl_category": "营业成本", "parent_account_id": "ACC6400", "account_level": 2, "is_leaf": True, "normal_balance": "借", "status": "启用"},
88
+ {"account_id": "ACC6600", "account_code": "6600", "account_name": "期间费用", "account_type": "费用", "pl_category": "期间费用", "parent_account_id": "", "account_level": 1, "is_leaf": False, "normal_balance": "借", "status": "启用"},
89
+ {"account_id": "ACC6601", "account_code": "6601", "account_name": "销售费用", "account_type": "费用", "pl_category": "期间费用", "parent_account_id": "ACC6600", "account_level": 2, "is_leaf": True, "normal_balance": "借", "status": "启用"},
90
+ {"account_id": "ACC6602", "account_code": "6602", "account_name": "管理费用", "account_type": "费用", "pl_category": "期间费用", "parent_account_id": "ACC6600", "account_level": 2, "is_leaf": True, "normal_balance": "借", "status": "启用"},
91
+ {"account_id": "ACC6603", "account_code": "6603", "account_name": "财务费用", "account_type": "费用", "pl_category": "期间费用", "parent_account_id": "ACC6600", "account_level": 2, "is_leaf": True, "normal_balance": "借", "status": "启用"},
92
+ ]
93
+ for a in accounts:
94
+ a["created_at"] = _SEED_DT
95
+
96
+ cost_centers = [
97
+ {"cost_center_id": "CC01", "cost_center_code": "PC-PROD", "cost_center_name": "生产中心", "department": "生产部", "company_code": "PD", "profit_center": "制造利润中心", "status": "启用"},
98
+ {"cost_center_id": "CC02", "cost_center_code": "PC-SALE", "cost_center_name": "销售中心", "department": "销售部", "company_code": "PD", "profit_center": "销售利润中心", "status": "启用"},
99
+ {"cost_center_id": "CC03", "cost_center_code": "PC-MGMT", "cost_center_name": "管理中心", "department": "管理部", "company_code": "PD", "profit_center": "管理利润中心", "status": "启用"},
100
+ {"cost_center_id": "CC04", "cost_center_code": "PC-RD", "cost_center_name": "研发中心", "department": "研发部", "company_code": "PD", "profit_center": "研发利润中心", "status": "启用"},
101
+ {"cost_center_id": "CC05", "cost_center_code": "PC-FIN", "cost_center_name": "财务中心", "department": "财务部", "company_code": "PD", "profit_center": "财务利润中心", "status": "启用"},
102
+ ]
103
+ for c in cost_centers:
104
+ c["created_at"] = _SEED_DT
105
+
106
+ s.sql.insert_rows("dim_account", accounts)
107
+ s.sql.insert_rows("dim_cost_center", cost_centers)
108
+ output.print("OK 维表数据")
109
+
110
+ leaf_accounts = [a for a in accounts if a["is_leaf"]]
111
+ acct_dict = {a["account_id"]: a for a in accounts}
112
+ cc_dict = {c["cost_center_id"]: c for c in cost_centers}
113
+
114
+ random.seed(9520)
115
+ fact_rows = []
116
+ seq = 1
117
+ start = date(2025, 1, 1)
118
+ end = date(2026, 6, 30)
119
+
120
+ base_amounts = {
121
+ "ACC6001": 800000,
122
+ "ACC6050": 50000,
123
+ "ACC6401": 480000,
124
+ "ACC6402": 120000,
125
+ "ACC6601": 80000,
126
+ "ACC6602": 60000,
127
+ "ACC6603": 20000,
128
+ }
129
+
130
+ d = start
131
+ while d <= end:
132
+ if d.day <= 3:
133
+ for acc_id, base in base_amounts.items():
134
+ account = acct_dict[acc_id]
135
+ for cc in cost_centers:
136
+ if account["account_type"] == "收入" and cc["cost_center_id"] not in ("CC01", "CC02"):
137
+ continue
138
+ if account["account_type"] == "成本" and cc["cost_center_id"] not in ("CC01", "CC04"):
139
+ continue
140
+ if account["account_type"] == "费用":
141
+ if account["account_id"] == "ACC6601" and cc["cost_center_id"] != "CC02":
142
+ continue
143
+ if account["account_id"] == "ACC6602" and cc["cost_center_id"] not in ("CC03", "CC04"):
144
+ continue
145
+ if account["account_id"] == "ACC6603" and cc["cost_center_id"] != "CC05":
146
+ continue
147
+ jitter = random.uniform(0.85, 1.15)
148
+ seasonal = 1.0 + 0.1 * ((d.month - 1) % 3)
149
+ amount = round(base * jitter * seasonal / max(len(cost_centers), 1) / 3, 2)
150
+ if amount <= 0:
151
+ continue
152
+ fact_rows.append(_make_entry_line(seq, d, account, cc, amount))
153
+ seq += 1
154
+ d += timedelta(days=1)
155
+
156
+ inserted = s.sql.insert_rows("fact_gl_journal_entry", fact_rows)
157
+ output.print(f"OK 实际分录表插入 {inserted} 行")
158
+
159
+ budget_rows = []
160
+ bseq = 1
161
+ for year in (2025, 2026):
162
+ version = _BUDGET_VERSION if year == 2026 else f"{year}年度预算"
163
+ for period in range(1, 13):
164
+ if year == 2026 and period > 6:
165
+ continue
166
+ for account in leaf_accounts:
167
+ base = base_amounts.get(account["account_id"], 50000)
168
+ for cc in cost_centers:
169
+ if account["account_type"] == "收入" and cc["cost_center_id"] not in ("CC01", "CC02"):
170
+ continue
171
+ if account["account_type"] == "成本" and cc["cost_center_id"] not in ("CC01", "CC04"):
172
+ continue
173
+ if account["account_type"] == "费用":
174
+ if account["account_id"] == "ACC6601" and cc["cost_center_id"] != "CC02":
175
+ continue
176
+ if account["account_id"] == "ACC6602" and cc["cost_center_id"] not in ("CC03", "CC04"):
177
+ continue
178
+ if account["account_id"] == "ACC6603" and cc["cost_center_id"] != "CC05":
179
+ continue
180
+ budget_amt = round(base / 6 * random.uniform(0.95, 1.05), 2)
181
+ date_key = int(f"{year}{period:02d}01")
182
+ budget_rows.append({
183
+ "budget_id": f"BUD{year}",
184
+ "line_id": f"BL{year}{period:02d}{bseq:05d}",
185
+ "date_key": date_key,
186
+ "budget_version": version,
187
+ "fiscal_year": year,
188
+ "fiscal_period": period,
189
+ "account_id": account["account_id"],
190
+ "account_code": account["account_code"],
191
+ "account_name": account["account_name"],
192
+ "account_type": account["account_type"],
193
+ "pl_category": account["pl_category"],
194
+ "cost_center_id": cc["cost_center_id"],
195
+ "cost_center_name": cc["cost_center_name"],
196
+ "department": cc["department"],
197
+ "budget_amount": budget_amt,
198
+ "currency": "CNY",
199
+ "status": "已发布",
200
+ "created_at": _SEED_DT,
201
+ })
202
+ bseq += 1
203
+
204
+ binserted = s.sql.insert_rows("fact_budget_entry", budget_rows)
205
+ output.print(f"OK 预算表插入 {binserted} 行")
206
+
207
+ summary = {
208
+ "ok": True,
209
+ "space_id": space_id,
210
+ "accounts": len(accounts),
211
+ "cost_centers": len(cost_centers),
212
+ "fact_inserted": inserted,
213
+ "budget_inserted": binserted,
214
+ }
215
+ output.success("灌数完成")
216
+ output.print("__JSON_SUMMARY__" + json.dumps(summary, ensure_ascii=True, default=str))
@@ -0,0 +1,89 @@
1
+ """渠道结构分析 sales.fn.channel_mix
2
+
3
+ 参数:start_date, end_date
4
+ 返回:各渠道销售额、销量、订单数及占比
5
+
6
+ 发布:
7
+ dazi onto script publish 项目/DAZI_TEST/本体/ontos/销售本体示例/functions/sales_fn_channel_mix.py \
8
+ --space space__misc_01 --register-function-id sales.fn.channel_mix
9
+ """
10
+
11
+ TEST_ARGUMENTS = {
12
+ "v": 1,
13
+ "arguments": {"start_date": "2025-01-01", "end_date": "2026-06-30"},
14
+ "object_type_code": "SalesAnalysis",
15
+ }
16
+
17
+
18
+ def _build_where(start_date, end_date):
19
+ clauses = ["order_status IN ('已完成', '已发货')"]
20
+ if start_date and end_date:
21
+ clauses.append(f"order_date >= '{start_date}' AND order_date <= '{end_date}'")
22
+ return "WHERE " + " AND ".join(clauses)
23
+
24
+
25
+ def _ontology_fn_body(p):
26
+ params = dict(p.get_params() or {})
27
+ start_date = params.get("start_date", "")
28
+ end_date = params.get("end_date", "")
29
+ where_clause = _build_where(start_date, end_date)
30
+
31
+ sql = f"""
32
+ SELECT
33
+ f.channel_id,
34
+ any(dch.channel_name) AS channel_name,
35
+ any(dch.channel_type) AS channel_type,
36
+ sum(f.sales_amount) AS sales_amount,
37
+ sum(f.quantity) AS quantity,
38
+ uniq(f.order_id) AS order_count
39
+ FROM fact_sales_order_line AS f
40
+ LEFT JOIN dim_channel AS dch ON f.channel_id = dch.channel_id
41
+ {where_clause}
42
+ GROUP BY f.channel_id
43
+ ORDER BY sales_amount DESC
44
+ """
45
+
46
+ result = p.sql.query(sql)
47
+ if not result:
48
+ return p.function_result(
49
+ columns=["channel_id", "channel_name", "channel_type", "sales_amount", "quantity", "order_count", "share_pct"],
50
+ data=[],
51
+ row_count=0,
52
+ )
53
+
54
+ total = sum(float(r.get("sales_amount", 0) or 0) for r in result)
55
+
56
+ data = []
57
+ for row in result:
58
+ sales_amount = float(row.get("sales_amount", 0) or 0)
59
+ share_pct = sales_amount / total if total > 0 else 0.0
60
+ data.append({
61
+ "channel_id": str(row.get("channel_id", "")),
62
+ "channel_name": str(row.get("channel_name", "")),
63
+ "channel_type": str(row.get("channel_type", "")),
64
+ "sales_amount": round(sales_amount, 2),
65
+ "quantity": int(row.get("quantity", 0) or 0),
66
+ "order_count": int(row.get("order_count", 0) or 0),
67
+ "share_pct": round(share_pct, 4),
68
+ })
69
+
70
+ return p.function_result(
71
+ columns=["channel_id", "channel_name", "channel_type", "sales_amount", "quantity", "order_count", "share_pct"],
72
+ data=data,
73
+ row_count=len(data),
74
+ )
75
+
76
+
77
+ def main():
78
+ s = space.get(ctx.space_id or "")
79
+ _Ports = type(
80
+ "_Ports",
81
+ (),
82
+ {
83
+ "get_params": lambda self: dict(ctx.params or {}),
84
+ "function_result": lambda self, **kw: onto.function_result(**kw),
85
+ },
86
+ )
87
+ p = _Ports()
88
+ p.sql = s.sql
89
+ return _ontology_fn_body(p)
@@ -0,0 +1,121 @@
1
+ """客户分层分析 sales.fn.customer_segmentation
2
+
3
+ 参数:metric(sales_amount|quantity), method(quartile|total), start_date, end_date
4
+ 返回:客户分层结果(VIP/战略/普通)
5
+
6
+ 发布:
7
+ dazi onto script publish 项目/DAZI_TEST/本体/ontos/销售本体示例/functions/sales_fn_customer_segmentation.py \
8
+ --space space__misc_01 --register-function-id sales.fn.customer_segmentation
9
+ """
10
+
11
+ TEST_ARGUMENTS = {
12
+ "v": 1,
13
+ "arguments": {
14
+ "metric": "sales_amount",
15
+ "method": "quartile",
16
+ "start_date": "2025-01-01",
17
+ "end_date": "2026-06-30",
18
+ },
19
+ "object_type_code": "Customer",
20
+ }
21
+
22
+
23
+ def _build_where(start_date, end_date):
24
+ clauses = ["order_status IN ('已完成', '已发货')"]
25
+ if start_date and end_date:
26
+ clauses.append(f"order_date >= '{start_date}' AND order_date <= '{end_date}'")
27
+ return "WHERE " + " AND ".join(clauses)
28
+
29
+
30
+ def _ontology_fn_body(p):
31
+ params = dict(p.get_params() or {})
32
+ metric = params.get("metric", "sales_amount")
33
+ method = params.get("method", "quartile")
34
+ start_date = params.get("start_date", "")
35
+ end_date = params.get("end_date", "")
36
+ where_clause = _build_where(start_date, end_date)
37
+
38
+ sql = f"""
39
+ SELECT
40
+ f.customer_id,
41
+ any(dc.customer_name) AS customer_name,
42
+ f.customer_type,
43
+ sum(f.sales_amount) AS sales_amount,
44
+ sum(f.quantity) AS quantity,
45
+ uniq(f.order_id) AS order_count
46
+ FROM fact_sales_order_line AS f
47
+ LEFT JOIN dim_customer AS dc ON f.customer_id = dc.customer_id
48
+ {where_clause}
49
+ GROUP BY f.customer_id, f.customer_type
50
+ ORDER BY sales_amount DESC
51
+ """
52
+
53
+ result = p.sql.query(sql)
54
+ if not result:
55
+ return p.function_result(
56
+ columns=["customer_id", "customer_name", "customer_type", "sales_amount", "quantity", "order_count", "segment"],
57
+ data=[],
58
+ row_count=0,
59
+ )
60
+
61
+ total_key = "quantity" if metric == "quantity" else "sales_amount"
62
+ total = sum(float(r.get(total_key, 0) or 0) for r in result)
63
+
64
+ if method == "quartile":
65
+ values = sorted([float(r.get(total_key, 0) or 0) for r in result], reverse=True)
66
+ n = len(values)
67
+ p75 = values[min(int(n * 0.25), n - 1)]
68
+ p50 = values[min(int(n * 0.50), n - 1)]
69
+ p25 = values[min(int(n * 0.75), n - 1)]
70
+
71
+ data = []
72
+ for row in result:
73
+ val = float(row.get(total_key, 0) or 0)
74
+ if method == "quartile":
75
+ if val >= p75:
76
+ segment = "VIP"
77
+ elif val >= p50:
78
+ segment = "战略"
79
+ elif val >= p25:
80
+ segment = "普通"
81
+ else:
82
+ segment = "低价值"
83
+ else:
84
+ share = val / total if total > 0 else 0
85
+ if share >= 0.6:
86
+ segment = "核心"
87
+ elif share >= 0.3:
88
+ segment = "重要"
89
+ else:
90
+ segment = "普通"
91
+
92
+ data.append({
93
+ "customer_id": str(row.get("customer_id", "")),
94
+ "customer_name": str(row.get("customer_name", "")),
95
+ "customer_type": str(row.get("customer_type", "")),
96
+ "sales_amount": round(float(row.get("sales_amount", 0) or 0), 2),
97
+ "quantity": int(row.get("quantity", 0) or 0),
98
+ "order_count": int(row.get("order_count", 0) or 0),
99
+ "segment": segment,
100
+ })
101
+
102
+ return p.function_result(
103
+ columns=["customer_id", "customer_name", "customer_type", "sales_amount", "quantity", "order_count", "segment"],
104
+ data=data,
105
+ row_count=len(data),
106
+ )
107
+
108
+
109
+ def main():
110
+ s = space.get(ctx.space_id or "")
111
+ _Ports = type(
112
+ "_Ports",
113
+ (),
114
+ {
115
+ "get_params": lambda self: dict(ctx.params or {}),
116
+ "function_result": lambda self, **kw: onto.function_result(**kw),
117
+ },
118
+ )
119
+ p = _Ports()
120
+ p.sql = s.sql
121
+ return _ontology_fn_body(p)
@@ -0,0 +1,78 @@
1
+ """销售总览函数 sales.fn.get_summary
2
+
3
+ 参数:start_date, end_date(可选)
4
+ 返回:总销售额、总销量、订单数、客单价、动销 SKU 数
5
+
6
+ 发布:
7
+ dazi onto script publish 项目/DAZI_TEST/本体/ontos/销售本体示例/functions/sales_fn_get_summary.py \
8
+ --space space__misc_01 --register-function-id sales.fn.get_summary
9
+ """
10
+
11
+ TEST_ARGUMENTS = {
12
+ "v": 1,
13
+ "arguments": {"start_date": "2025-01-01", "end_date": "2026-06-30"},
14
+ "object_type_code": "SalesAnalysis",
15
+ }
16
+
17
+
18
+ def _valid_order_clause():
19
+ return "order_status IN ('已完成', '已发货')"
20
+
21
+
22
+ def _build_where(start_date, end_date):
23
+ clauses = [_valid_order_clause()]
24
+ if start_date and end_date:
25
+ clauses.append(f"order_date >= '{start_date}' AND order_date <= '{end_date}'")
26
+ return "WHERE " + " AND ".join(clauses)
27
+
28
+
29
+ def _ontology_fn_body(p):
30
+ params = dict(p.get_params() or {})
31
+ start_date = params.get("start_date", "")
32
+ end_date = params.get("end_date", "")
33
+ where_clause = _build_where(start_date, end_date)
34
+
35
+ sql = f"""
36
+ SELECT
37
+ sum(sales_amount) AS total_sales,
38
+ sum(quantity) AS total_quantity,
39
+ uniq(order_id) AS order_count,
40
+ uniq(product_id) AS product_count
41
+ FROM fact_sales_order_line
42
+ {where_clause}
43
+ """
44
+
45
+ rows = p.sql.query(sql)
46
+ row = rows[0] if rows else {}
47
+ total_sales = float(row.get("total_sales", 0) or 0)
48
+ order_count = int(row.get("order_count", 0) or 0)
49
+ avg_order_value = total_sales / order_count if order_count > 0 else 0.0
50
+
51
+ data = [{
52
+ "total_sales": round(total_sales, 2),
53
+ "total_quantity": int(row.get("total_quantity", 0) or 0),
54
+ "order_count": order_count,
55
+ "avg_order_value": round(avg_order_value, 2),
56
+ "product_count": int(row.get("product_count", 0) or 0),
57
+ }]
58
+
59
+ return p.function_result(
60
+ columns=["total_sales", "total_quantity", "order_count", "avg_order_value", "product_count"],
61
+ data=data,
62
+ row_count=1,
63
+ )
64
+
65
+
66
+ def main():
67
+ s = space.get(ctx.space_id or "")
68
+ _Ports = type(
69
+ "_Ports",
70
+ (),
71
+ {
72
+ "get_params": lambda self: dict(ctx.params or {}),
73
+ "function_result": lambda self, **kw: onto.function_result(**kw),
74
+ },
75
+ )
76
+ p = _Ports()
77
+ p.sql = s.sql
78
+ return _ontology_fn_body(p)
@@ -0,0 +1,89 @@
1
+ """月度环比分析 sales.fn.mom_analysis
2
+
3
+ 参数:start_date, end_date
4
+ 返回:月度销售额、销量、订单数及环比增长率
5
+
6
+ 发布:
7
+ dazi onto script publish 项目/DAZI_TEST/本体/ontos/销售本体示例/functions/sales_fn_mom_analysis.py \
8
+ --space space__misc_01 --register-function-id sales.fn.mom_analysis
9
+ """
10
+
11
+ TEST_ARGUMENTS = {
12
+ "v": 1,
13
+ "arguments": {"start_date": "2025-01-01", "end_date": "2026-06-30"},
14
+ "object_type_code": "SalesAnalysis",
15
+ }
16
+
17
+
18
+ def _build_where(start_date, end_date):
19
+ clauses = ["order_status IN ('已完成', '已发货')"]
20
+ if start_date and end_date:
21
+ clauses.append(f"order_date >= '{start_date}' AND order_date <= '{end_date}'")
22
+ return "WHERE " + " AND ".join(clauses)
23
+
24
+
25
+ def _ontology_fn_body(p):
26
+ params = dict(p.get_params() or {})
27
+ start_date = params.get("start_date", "")
28
+ end_date = params.get("end_date", "")
29
+ where_clause = _build_where(start_date, end_date)
30
+
31
+ sql = f"""
32
+ SELECT
33
+ toYear(order_date) AS year,
34
+ toMonth(order_date) AS month,
35
+ sum(sales_amount) AS sales_amount,
36
+ sum(quantity) AS quantity,
37
+ uniq(order_id) AS order_count
38
+ FROM fact_sales_order_line
39
+ {where_clause}
40
+ GROUP BY toYear(order_date), toMonth(order_date)
41
+ ORDER BY year, month
42
+ """
43
+
44
+ result = p.sql.query(sql)
45
+ if not result:
46
+ return p.function_result(
47
+ columns=["year", "month", "sales_amount", "quantity", "order_count", "mom_growth"],
48
+ data=[],
49
+ row_count=0,
50
+ )
51
+
52
+ data = []
53
+ prev_sales = None
54
+ for row in result:
55
+ sales_amount = float(row.get("sales_amount", 0) or 0)
56
+ if prev_sales is not None and prev_sales != 0:
57
+ mom_growth = (sales_amount - prev_sales) / prev_sales
58
+ else:
59
+ mom_growth = 0.0
60
+ data.append({
61
+ "year": int(row.get("year", 0)),
62
+ "month": int(row.get("month", 0)),
63
+ "sales_amount": round(sales_amount, 2),
64
+ "quantity": int(row.get("quantity", 0) or 0),
65
+ "order_count": int(row.get("order_count", 0) or 0),
66
+ "mom_growth": round(mom_growth, 4),
67
+ })
68
+ prev_sales = sales_amount
69
+
70
+ return p.function_result(
71
+ columns=["year", "month", "sales_amount", "quantity", "order_count", "mom_growth"],
72
+ data=data,
73
+ row_count=len(data),
74
+ )
75
+
76
+
77
+ def main():
78
+ s = space.get(ctx.space_id or "")
79
+ _Ports = type(
80
+ "_Ports",
81
+ (),
82
+ {
83
+ "get_params": lambda self: dict(ctx.params or {}),
84
+ "function_result": lambda self, **kw: onto.function_result(**kw),
85
+ },
86
+ )
87
+ p = _Ports()
88
+ p.sql = s.sql
89
+ return _ontology_fn_body(p)