@taopao/memoir 0.0.0-stage → 0.1.0

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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -2
  3. package/assets/migrations-pg/0100_baseline.sql +1115 -0
  4. package/assets/migrations-pg/0101_functions.sql +93 -0
  5. package/assets/migrations-pg/0102_views.sql +2425 -0
  6. package/assets/migrations-pg/0103_constraints.sql +147 -0
  7. package/assets/migrations-pg/0104_protection.sql +33 -0
  8. package/assets/migrations-pg/0105_bocom_credit_card.sql +43 -0
  9. package/assets/migrations-pg/0106_psbc_debit_card.sql +87 -0
  10. package/assets/migrations-pg/0107_tailong_debit_card.sql +87 -0
  11. package/assets/migrations-pg/0108_ccb_debit_card.sql +87 -0
  12. package/assets/migrations-pg/0109_ccb_credit_card.sql +63 -0
  13. package/assets/migrations-pg/0110_icbc_debit_card.sql +87 -0
  14. package/assets/migrations-pg/0111_icbc_credit_card.sql +66 -0
  15. package/assets/migrations-pg/0112_boc_debit_card.sql +88 -0
  16. package/assets/migrations-pg/0113_jsb_debit_card.sql +88 -0
  17. package/assets/migrations-pg/0114_cmbc_debit_card.sql +88 -0
  18. package/assets/migrations-pg/0115_hzbank_debit_card.sql +88 -0
  19. package/assets/migrations-pg/0116_unified_bank_documents.sql +197 -0
  20. package/assets/migrations-pg/0117_agent_submissions.sql +50 -0
  21. package/assets/migrations-pg/0118_agent_reversions_analysis.sql +34 -0
  22. package/assets/migrations-pg/0119_agent_extension_registry.sql +72 -0
  23. package/assets/migrations-pg/0120_agent_record_sources.sql +22 -0
  24. package/assets/migrations-pg/0121_agent_extension_deprecation.sql +30 -0
  25. package/assets/migrations-pg/0122_agent_extension_controlled_revert.sql +107 -0
  26. package/assets/migrations-pg/0123_agent_extension_versions.sql +54 -0
  27. package/assets/migrations-pg/0124_agent_extension_retirement_groups.sql +26 -0
  28. package/assets/migrations-pg/0125_agent_candidate_reviews.sql +30 -0
  29. package/assets/migrations-pg/0126_agent_module_lifecycle.sql +93 -0
  30. package/assets/migrations-pg/0127_agent_tasks.sql +64 -0
  31. package/assets/migrations-pg/0128_agent_task_leases.sql +80 -0
  32. package/assets/migrations-pg/0129_agent_task_plans.sql +66 -0
  33. package/assets/migrations-pg/0130_agent_task_submissions.sql +72 -0
  34. package/assets/migrations-pg/0131_agent_task_completions.sql +93 -0
  35. package/assets/migrations-pg/0132_agent_task_changes.sql +113 -0
  36. package/assets/migrations-pg/0133_agent_imported_source_snapshots.sql +152 -0
  37. package/assets/migrations-pg/0134_agent_analysis_datasets.sql +6 -0
  38. package/assets/migrations-pg/0135_agent_maintenance_tasks.sql +139 -0
  39. package/assets/migrations-pg/0136_agent_task_structures.sql +66 -0
  40. package/assets/migrations-pg/0137_agent_personal_identity.sql +149 -0
  41. package/assets/migrations-pg/0138_agent_work_records.sql +24 -0
  42. package/assets/migrations-pg/0139_shared_files.sql +43 -0
  43. package/assets/migrations-pg/0140_shared_script_runs.sql +36 -0
  44. package/assets/migrations-pg/0141_payment_bank_links.sql +96 -0
  45. package/assets/migrations-pg/0142_consumption_order_types_and_places.sql +287 -0
  46. package/assets/migrations-pg/0143_skill_direct_write.sql +330 -0
  47. package/assets/migrations-pg/0144_skill_schema_archive.sql +73 -0
  48. package/assets/migrations-pg/0145_skill_ingest_wiki.sql +91 -0
  49. package/assets/migrations-pg/0146_skill_load.sql +17 -0
  50. package/assets/migrations-pg/0147_drop_agent_schema.sql +147 -0
  51. package/assets/migrations-pg/0148_ingest_lanes.sql +19 -0
  52. package/assets/migrations-pg/0149_account_ownership.sql +233 -0
  53. package/assets/migrations-pg/0150_bank_card_exact_link.sql +24 -0
  54. package/assets/migrations-pg/0151_account_ownership_enforce.sql +35 -0
  55. package/assets/migrations-pg/0152_account_ownership_views.sql +750 -0
  56. package/assets/migrations-pg/0153_web_frontend.sql +34 -0
  57. package/assets/migrations-pg/0154_audio_record_owners.sql +103 -0
  58. package/assets/migrations-pg/0155_ingest_knowledge_tables.sql +171 -0
  59. package/assets/migrations-pg/0156_batch_playbook_note.sql +17 -0
  60. package/assets/migrations-pg/0157_last_contact.sql +166 -0
  61. package/assets/migrations-pg/0158_refund_match_hash_join.sql +91 -0
  62. package/assets/migrations-pg/0159_fact_occurred_time.sql +88 -0
  63. package/assets/migrations-pg/0160_agent_notes.sql +118 -0
  64. package/assets/migrations-pg/0161_life_history.sql +314 -0
  65. package/assets/migrations-pg/0162_person_card.sql +427 -0
  66. package/assets/migrations-pg/0163_parking_drop_personal_defaults.sql +12 -0
  67. package/assets/native/darwin-arm64.node +0 -0
  68. package/assets/playbooks/audio.md +83 -0
  69. package/assets/playbooks/chat.md +37 -0
  70. package/assets/playbooks/common.md +66 -0
  71. package/assets/playbooks/intimate.md +19 -0
  72. package/assets/playbooks/journal.md +17 -0
  73. package/assets/playbooks/orders.md +15 -0
  74. package/assets/playbooks/payments.md +16 -0
  75. package/assets/playbooks/self.md +29 -0
  76. package/bin/install-codex-skill.mjs +77 -0
  77. package/bin/memoir.mjs +5 -0
  78. package/dist/cli.js +34062 -0
  79. package/package.json +40 -4
  80. package/release-manifest.json +432 -0
  81. package/skill/SKILL.md +120 -0
  82. package/skill/references//345/256/211/350/243/205/344/275/215/347/275/256.md +21 -0
  83. package/skill/references//346/225/260/346/215/256/345/217/243/345/276/204.md +36 -0
  84. package/skill/scripts/memoir.mjs +82 -0
@@ -0,0 +1,2425 @@
1
+ -- 视图金额沿用 SQLite 浮点比例;向零截断后转 BIGINT,ROUND 使用 numeric 保持半分舍入。
2
+ /* 视图保持原有返回列;CAST 浮点金额先向零截断,整数使用 BIGINT,ROUND 显式使用 numeric 保持半分舍入。 */
3
+ /* _v_annotations */
4
+ /* 视图保持原有返回列;方言差异在此显式处理。 */
5
+ /* _v_annotations */
6
+ CREATE VIEW _v_annotations AS
7
+ WITH active AS (
8
+ SELECT
9
+ *,
10
+ ROW_NUMBER() OVER (
11
+ PARTITION BY target_type, target_key, kind
12
+ ORDER BY CASE WHEN basis = 'manual' OR updated_by = 'me' THEN 0 ELSE 1 END NULLS FIRST, updated_at DESC NULLS LAST, id DESC NULLS LAST
13
+ ) AS priority
14
+ FROM labels
15
+ WHERE
16
+ deleted_at IS NULL
17
+ )
18
+ SELECT
19
+ target_type,
20
+ target_key,
21
+ MAX(
22
+ CASE
23
+ WHEN kind = 'hidden' AND NOT LOWER("value") IN ('0', 'false', 'no', '否')
24
+ THEN 1
25
+ ELSE 0
26
+ END
27
+ ) AS hidden,
28
+ MAX(CASE WHEN kind = 'category' AND priority = 1 THEN "value" END) AS category,
29
+ MAX(
30
+ CASE
31
+ WHEN kind = 'sensitivity'
32
+ AND priority = 1
33
+ AND "value" IN ('normal', 'private', 'intimate')
34
+ THEN "value"
35
+ END
36
+ ) AS sensitivity
37
+ FROM active
38
+ GROUP BY
39
+ target_type,
40
+ target_key;
41
+
42
+ /* _v_beneficiary_groups */
43
+ /* _v_beneficiary_groups */
44
+ CREATE VIEW _v_beneficiary_groups AS
45
+ WITH active AS (
46
+ SELECT
47
+ b.*
48
+ FROM beneficiaries AS b
49
+ JOIN persons AS p
50
+ ON p.id = b.person_id
51
+ WHERE
52
+ b.deleted_at IS NULL AND p.deleted_at IS NULL
53
+ ), shares AS (
54
+ SELECT
55
+ *,
56
+ CASE
57
+ WHEN NOT share_ratio IS NULL
58
+ THEN share_ratio
59
+ ELSE GREATEST(
60
+ 0.0,
61
+ CAST(1.0 AS DOUBLE PRECISION) - SUM(COALESCE(share_ratio, 0)) OVER (PARTITION BY target_type, target_key)
62
+ ) / NULLIF(
63
+ SUM(CAST(share_ratio IS NULL AS INT)) OVER (PARTITION BY target_type, target_key),
64
+ 0
65
+ )
66
+ END AS effective_share
67
+ FROM active
68
+ )
69
+ SELECT
70
+ target_type,
71
+ target_key,
72
+ CAST(JSON_AGG(JSON_OBJECT('person_id': person_id, 'share_ratio': effective_share)) AS TEXT) AS beneficiaries_json
73
+ FROM (
74
+ SELECT
75
+ *
76
+ FROM shares
77
+ ORDER BY
78
+ person_id NULLS FIRST
79
+ )
80
+ GROUP BY
81
+ target_type,
82
+ target_key;
83
+
84
+ /* _v_date_occurrences */
85
+ /* _v_date_occurrences */
86
+ CREATE VIEW _v_date_occurrences AS
87
+ WITH RECURSIVE years(year) AS (
88
+ SELECT
89
+ CAST(SUBSTRING(VAULT_TODAY() FROM 1 FOR 4) AS BIGINT) - 1
90
+ UNION ALL
91
+ SELECT
92
+ year + 1
93
+ FROM years
94
+ WHERE
95
+ year < CAST(SUBSTRING(VAULT_TODAY() FROM 1 FOR 4) AS BIGINT) + 8
96
+ ), dates AS (
97
+ SELECT
98
+ d.*
99
+ FROM important_dates AS d
100
+ LEFT JOIN _v_annotations AS a
101
+ ON a.target_type = 'important_dates' AND a.target_key = d.id
102
+ WHERE
103
+ d.deleted_at IS NULL AND d.status = 'confirmed' AND COALESCE(a.hidden, 0) = 0
104
+ )
105
+ SELECT
106
+ d.*,
107
+ VAULT_CALENDAR_DATE(d.calendar, d.date_value, y.year) AS occurs_on
108
+ FROM dates AS d
109
+ CROSS JOIN years AS y
110
+ WHERE
111
+ d.recurrence = 'yearly'
112
+ UNION ALL
113
+ SELECT
114
+ d.*,
115
+ COALESCE(
116
+ VAULT_CALENDAR_DATE(d.calendar, d.date_value, NULL),
117
+ CASE WHEN d.calendar = 'lunar' THEN d.first_on END
118
+ ) AS occurs_on
119
+ FROM dates AS d
120
+ WHERE
121
+ d.recurrence = 'none';
122
+
123
+ /* _v_module_people */
124
+ /* _v_module_people */
125
+ CREATE VIEW _v_module_people AS
126
+ SELECT
127
+ id,
128
+ CASE WHEN is_self = 1 THEN 'me' ELSE 'her' END AS module
129
+ FROM persons
130
+ WHERE
131
+ deleted_at IS NULL AND (
132
+ is_self = 1 OR is_default = 1
133
+ );
134
+
135
+ /* _v_order_item_weights */
136
+ /* _v_order_item_weights */
137
+ CREATE VIEW _v_order_item_weights AS
138
+ WITH items AS (
139
+ SELECT
140
+ i.*,
141
+ COALESCE(item_amount_cents, unit_price_cents * COALESCE(quantity, 1)) AS gross_cents,
142
+ GREATEST(0, COALESCE(item_amount_cents, unit_price_cents * COALESCE(quantity, 1), 1)) AS raw_weight,
143
+ CASE
144
+ WHEN item_status IN ('退款成功', '已退款', '已全额退款', '退货退款成功', '交易关闭', '已关闭', '已取消')
145
+ THEN 1
146
+ ELSE 0
147
+ END AS refunded
148
+ FROM order_items AS i
149
+ )
150
+ SELECT
151
+ *,
152
+ CASE
153
+ WHEN SUM(raw_weight) OVER (PARTITION BY platform, order_no) = 0
154
+ THEN 1
155
+ ELSE raw_weight
156
+ END AS weight
157
+ FROM items;
158
+
159
+ /* _v_payment_bases */
160
+ /* _v_payment_bases */
161
+ CREATE VIEW _v_payment_bases AS
162
+ SELECT
163
+ p.*,
164
+ p.platform || ':' || p.txn_id AS target_key,
165
+ COALESCE(
166
+ NULLIF(
167
+ TRIM(REPLACE(CAST(JSON_EXTRACT_PATH_TEXT(p.raw_json, '交易订单号') AS TEXT), CHR( 9), '')),
168
+ ''
169
+ ),
170
+ p.txn_id
171
+ ) AS txn_base,
172
+ CASE
173
+ WHEN p.direction <> 'expense'
174
+ AND (
175
+ p.status IN ('退款成功', '已退款', '已全额退款', '对方已退还')
176
+ OR (
177
+ (
178
+ p.platform_category = '退款' OR p.txn_type = '退款'
179
+ )
180
+ AND p.status IN ('成功', '交易成功', '支付成功')
181
+ )
182
+ )
183
+ THEN 1
184
+ ELSE 0
185
+ END AS is_refund
186
+ FROM payments AS p;
187
+
188
+ /* _v_payment_refund_matches */
189
+ /* _v_payment_refund_matches */
190
+ CREATE VIEW _v_payment_refund_matches AS
191
+ WITH candidates AS (
192
+ SELECT
193
+ e.platform,
194
+ e.txn_id,
195
+ r.target_key AS refund_key,
196
+ r.amount_cents,
197
+ CASE WHEN e.txn_base = r.txn_base THEN 0 ELSE 1 END AS priority
198
+ FROM _v_payment_bases AS e
199
+ JOIN _v_payment_bases AS r
200
+ ON r.platform = e.platform
201
+ AND r.is_refund = 1
202
+ AND e.direction = 'expense'
203
+ AND r.occurred_at >= e.occurred_at
204
+ AND (
205
+ e.txn_base = r.txn_base
206
+ OR (
207
+ NOT NULLIF(TRIM(e.merchant_order_no), '') IS NULL
208
+ AND e.merchant_order_no <> '/'
209
+ AND TRIM(e.merchant_order_no) = TRIM(r.merchant_order_no)
210
+ )
211
+ )
212
+ ), ranked AS (
213
+ SELECT
214
+ *,
215
+ MIN(priority) OVER (PARTITION BY refund_key) AS best
216
+ FROM candidates
217
+ ), unique_matches AS (
218
+ SELECT
219
+ *,
220
+ COUNT(*) OVER (PARTITION BY refund_key) AS matches
221
+ FROM ranked
222
+ WHERE
223
+ priority = best
224
+ )
225
+ SELECT
226
+ platform,
227
+ txn_id,
228
+ refund_key,
229
+ amount_cents
230
+ FROM unique_matches
231
+ WHERE
232
+ matches = 1;
233
+
234
+ /* _v_trusted_payment_orders */
235
+ /* _v_trusted_payment_orders */
236
+ CREATE VIEW _v_trusted_payment_orders AS
237
+ SELECT
238
+ *
239
+ FROM payment_order_links
240
+ WHERE
241
+ deleted_at IS NULL AND (
242
+ confidence = 'high' OR match_method = 'manual'
243
+ );
244
+
245
+ /* v_payments */
246
+ /* v_payments */
247
+ CREATE VIEW v_payments AS
248
+ WITH identity_candidates AS (
249
+ SELECT
250
+ p.target_key,
251
+ i.person_id,
252
+ CASE WHEN i.match_kind = 'account_id' THEN 0 ELSE 1 END AS priority
253
+ FROM _v_payment_bases AS p
254
+ JOIN identities AS i
255
+ ON i.source = p.platform
256
+ AND i.deleted_at IS NULL
257
+ AND (
258
+ (
259
+ i.match_kind = 'account_id'
260
+ AND VAULT_IDENTITY_MATCH(i.match_mode, i."value", p.counterparty_account) = 1
261
+ )
262
+ OR (
263
+ i.match_kind = 'counterparty'
264
+ AND VAULT_IDENTITY_MATCH(i.match_mode, i."value", p.counterparty) = 1
265
+ )
266
+ )
267
+ AND (
268
+ i.valid_from IS NULL OR i.valid_from <= SUBSTRING(p.occurred_at FROM 1 FOR 10)
269
+ )
270
+ AND (
271
+ i.valid_to IS NULL OR i.valid_to >= SUBSTRING(p.occurred_at FROM 1 FOR 10)
272
+ )
273
+ JOIN persons AS person
274
+ ON person.id = i.person_id AND person.deleted_at IS NULL
275
+ ), ranked AS (
276
+ SELECT
277
+ *,
278
+ MIN(priority) OVER (PARTITION BY target_key) AS best
279
+ FROM identity_candidates
280
+ ), matched AS (
281
+ SELECT
282
+ target_key,
283
+ MIN(person_id) AS person_id,
284
+ COUNT(DISTINCT person_id) AS persons
285
+ FROM ranked
286
+ WHERE
287
+ priority = best
288
+ GROUP BY
289
+ target_key
290
+ ), refunds AS (
291
+ SELECT
292
+ platform,
293
+ txn_id,
294
+ SUM(amount_cents) AS refund_cents
295
+ FROM _v_payment_refund_matches
296
+ GROUP BY
297
+ platform,
298
+ txn_id
299
+ ), enriched AS (
300
+ SELECT
301
+ p.*,
302
+ CASE WHEN i.persons = 1 THEN i.person_id END AS counterparty_person_id,
303
+ CASE WHEN i.persons = 1 THEN person.display_name END AS counterparty_person_name,
304
+ COALESCE(CAST(i.persons > 1 AS INT), 0) AS identity_ambiguous,
305
+ a.category AS analysis_category,
306
+ COALESCE(a.hidden, 0) AS hidden,
307
+ COALESCE(a.sensitivity, 'private') AS sensitivity,
308
+ LEAST(
309
+ p.amount_cents,
310
+ GREATEST(
311
+ COALESCE(p.refund_cents, 0),
312
+ COALESCE(r.refund_cents, 0),
313
+ CASE
314
+ WHEN p.status IN ('已全额退款', '对方已退还', '已退还', '退款成功', '已退款')
315
+ THEN p.amount_cents
316
+ ELSE 0
317
+ END
318
+ )
319
+ ) AS effective_refund_cents,
320
+ CASE
321
+ WHEN POSITION(
322
+ '转账' IN COALESCE(p.txn_type, '') || ' ' || COALESCE(p.platform_category, '') || ' ' || COALESCE(a.category, '')
323
+ ) > 0
324
+ OR POSITION(
325
+ '红包' IN COALESCE(p.txn_type, '') || ' ' || COALESCE(p.platform_category, '') || ' ' || COALESCE(a.category, '')
326
+ ) > 0
327
+ THEN 1
328
+ ELSE 0
329
+ END AS is_transfer
330
+ FROM _v_payment_bases AS p
331
+ LEFT JOIN refunds AS r
332
+ ON r.platform = p.platform AND r.txn_id = p.txn_id
333
+ LEFT JOIN matched AS i
334
+ ON i.target_key = p.target_key
335
+ LEFT JOIN persons AS person
336
+ ON person.id = i.person_id
337
+ LEFT JOIN _v_annotations AS a
338
+ ON a.target_type = 'payment' AND a.target_key = p.target_key
339
+ )
340
+ SELECT
341
+ *,
342
+ amount_cents / NULLIF(100.0, 0) AS amount_yuan,
343
+ CASE
344
+ WHEN status IN ('交易关闭', '已关闭', '已取消', '待付款', '等待买家付款')
345
+ THEN 0
346
+ ELSE amount_cents - effective_refund_cents
347
+ END AS net_amount_cents,
348
+ CASE
349
+ WHEN status IN ('交易关闭', '已关闭', '已取消', '待付款', '等待买家付款')
350
+ THEN 0
351
+ ELSE (
352
+ amount_cents - effective_refund_cents
353
+ ) / NULLIF(100.0, 0)
354
+ END AS net_amount_yuan
355
+ FROM enriched;
356
+
357
+ /* _v_order_payment_allocations */
358
+ /* _v_order_payment_allocations */
359
+ CREATE VIEW _v_order_payment_allocations AS
360
+ WITH weights AS (
361
+ SELECT
362
+ l.*,
363
+ p.amount_cents,
364
+ p.effective_refund_cents,
365
+ p.direction,
366
+ p.is_refund,
367
+ p.hidden,
368
+ CASE WHEN o.total_paid_cents > 0 THEN o.total_paid_cents ELSE 1 END AS weight,
369
+ CASE
370
+ WHEN p.is_refund = 1
371
+ AND NOT EXISTS(
372
+ SELECT
373
+ 1
374
+ FROM _v_payment_refund_matches AS r
375
+ WHERE
376
+ r.refund_key = p.target_key
377
+ )
378
+ THEN p.amount_cents
379
+ WHEN p.direction = 'expense'
380
+ THEN p.effective_refund_cents
381
+ ELSE 0
382
+ END AS refund_cents
383
+ FROM _v_trusted_payment_orders AS l
384
+ JOIN v_payments AS p
385
+ ON p.platform = l.payment_platform AND p.txn_id = l.payment_txn_id
386
+ JOIN orders AS o
387
+ ON o.platform = l.order_platform AND o.order_no = l.order_no
388
+ ), proportions AS (
389
+ SELECT
390
+ *,
391
+ SUM(weight) OVER (PARTITION BY payment_platform, payment_txn_id) AS total_weight,
392
+ ROW_NUMBER() OVER (
393
+ PARTITION BY payment_platform, payment_txn_id
394
+ ORDER BY order_platform NULLS FIRST, order_no NULLS FIRST
395
+ ) AS ordinal
396
+ FROM weights
397
+ ), rounded AS (
398
+ SELECT
399
+ *,
400
+ CAST(TRUNC(amount_cents * CAST(1.0 AS DOUBLE PRECISION) * weight / NULLIF(total_weight, 0)) AS BIGINT) AS base_cents,
401
+ CAST(TRUNC(refund_cents * CAST(1.0 AS DOUBLE PRECISION) * weight / NULLIF(total_weight, 0)) AS BIGINT) AS base_refund_cents
402
+ FROM proportions
403
+ )
404
+ SELECT
405
+ *,
406
+ base_cents + CASE
407
+ WHEN ordinal = 1
408
+ THEN amount_cents - SUM(base_cents) OVER (PARTITION BY payment_platform, payment_txn_id)
409
+ ELSE 0
410
+ END AS allocated_cents,
411
+ base_refund_cents + CASE
412
+ WHEN ordinal = 1
413
+ THEN refund_cents - SUM(base_refund_cents) OVER (PARTITION BY payment_platform, payment_txn_id)
414
+ ELSE 0
415
+ END AS allocated_refund_cents
416
+ FROM rounded;
417
+
418
+ /* _v_order_net */
419
+ /* _v_order_net */
420
+ CREATE VIEW _v_order_net AS
421
+ WITH paid AS (
422
+ SELECT
423
+ order_platform,
424
+ order_no,
425
+ SUM(CASE WHEN direction = 'expense' THEN allocated_cents END) AS gross_cents,
426
+ SUM(allocated_refund_cents) AS refund_cents,
427
+ MAX(hidden) AS payment_hidden,
428
+ COUNT(
429
+ DISTINCT CASE WHEN direction = 'expense' THEN payment_platform || ':' || payment_txn_id END
430
+ ) AS payment_count,
431
+ MIN(
432
+ CASE WHEN direction = 'expense' THEN payment_platform || ':' || payment_txn_id END
433
+ ) AS payment_key
434
+ FROM _v_order_payment_allocations
435
+ GROUP BY
436
+ order_platform,
437
+ order_no
438
+ ), items AS (
439
+ SELECT
440
+ platform,
441
+ order_no,
442
+ CASE WHEN COUNT(gross_cents) = COUNT(*) THEN SUM(gross_cents) END AS gross_cents,
443
+ SUM(weight) AS total_weight,
444
+ SUM(CASE WHEN refunded = 1 THEN weight ELSE 0 END) AS refunded_weight
445
+ FROM _v_order_item_weights
446
+ GROUP BY
447
+ platform,
448
+ order_no
449
+ ), amounts AS (
450
+ SELECT
451
+ o.*,
452
+ COALESCE(o.total_paid_cents, p.gross_cents, o.goods_total_cents, i.gross_cents) AS gross_cents,
453
+ COALESCE(p.refund_cents, 0) AS payment_refund_cents,
454
+ COALESCE(p.payment_hidden, 0) AS payment_hidden,
455
+ COALESCE(p.payment_count, 0) AS payment_count,
456
+ p.payment_key,
457
+ COALESCE(
458
+ i.refunded_weight * CAST(1.0 AS DOUBLE PRECISION) / NULLIF(NULLIF(i.total_weight, 0), 0),
459
+ 0
460
+ ) AS item_refund_ratio,
461
+ CASE
462
+ WHEN NOT o.total_paid_cents IS NULL
463
+ THEN 'order_paid'
464
+ WHEN NOT p.gross_cents IS NULL
465
+ THEN 'linked_payment'
466
+ WHEN NOT o.goods_total_cents IS NULL
467
+ THEN 'order_goods'
468
+ WHEN NOT i.gross_cents IS NULL
469
+ THEN 'item_total'
470
+ ELSE 'unknown'
471
+ END AS amount_basis
472
+ FROM orders AS o
473
+ LEFT JOIN paid AS p
474
+ ON p.order_platform = o.platform AND p.order_no = o.order_no
475
+ LEFT JOIN items AS i
476
+ ON i.platform = o.platform AND i.order_no = o.order_no
477
+ )
478
+ SELECT
479
+ *,
480
+ CASE
481
+ WHEN status IN ('交易关闭', '已关闭', '已取消', '待付款', '等待买家付款', '退款成功', '已退款', '已全额退款', '退货退款成功')
482
+ THEN 0
483
+ WHEN gross_cents IS NULL
484
+ THEN NULL
485
+ ELSE GREATEST(
486
+ 0,
487
+ gross_cents - GREATEST(
488
+ payment_refund_cents,
489
+ CAST(TRUNC(ROUND(CAST(gross_cents * item_refund_ratio AS DECIMAL))) AS BIGINT)
490
+ )
491
+ )
492
+ END AS net_cents,
493
+ CASE
494
+ WHEN payment_refund_cents > 0
495
+ AND payment_refund_cents >= ROUND(CAST(gross_cents * item_refund_ratio AS DECIMAL))
496
+ THEN 'payment'
497
+ WHEN item_refund_ratio > 0
498
+ THEN 'item_status_allocation'
499
+ ELSE 'none'
500
+ END AS refund_basis,
501
+ CASE
502
+ WHEN amount_basis IN ('order_goods', 'item_total', 'unknown')
503
+ OR ROUND(CAST(gross_cents * item_refund_ratio AS DECIMAL)) > payment_refund_cents
504
+ THEN 1
505
+ ELSE 0
506
+ END AS amount_is_estimated
507
+ FROM amounts;
508
+
509
+ /* v_spending */
510
+ /* v_spending */
511
+ CREATE VIEW v_spending AS
512
+ WITH order_lines AS (
513
+ SELECT
514
+ o.platform,
515
+ o.order_no,
516
+ i.item_key,
517
+ COALESCE(o.paid_at, o.ordered_at) AS occurred_at,
518
+ COALESCE(i.product_name, o.shop_name, '订单') AS title,
519
+ o.shop_name,
520
+ o.gross_cents AS order_gross_cents,
521
+ o.net_cents,
522
+ o.amount_basis,
523
+ o.refund_basis,
524
+ o.amount_is_estimated,
525
+ COALESCE(i.weight, 1) AS weight,
526
+ o.payment_count,
527
+ o.payment_key,
528
+ GREATEST(COALESCE(oa.hidden, 0), COALESCE(ia.hidden, 0), o.payment_hidden) AS hidden,
529
+ COALESCE(ia.category, oa.category, pa.category) AS category,
530
+ CASE
531
+ WHEN ia.sensitivity = 'intimate'
532
+ OR oa.sensitivity = 'intimate'
533
+ OR pa.sensitivity = 'intimate'
534
+ THEN 'intimate'
535
+ ELSE COALESCE(ia.sensitivity, oa.sensitivity, pa.sensitivity, 'private')
536
+ END AS sensitivity,
537
+ CAST(COALESCE(
538
+ ib.beneficiaries_json,
539
+ ob.beneficiaries_json,
540
+ CASE WHEN o.payment_count = 1 THEN pb.beneficiaries_json END,
541
+ '[]'
542
+ ) AS TEXT) AS beneficiaries_json
543
+ FROM _v_order_net AS o
544
+ LEFT JOIN _v_order_item_weights AS i
545
+ ON i.platform = o.platform AND i.order_no = o.order_no
546
+ LEFT JOIN _v_annotations AS oa
547
+ ON oa.target_type = 'order' AND oa.target_key = o.platform || ':' || o.order_no
548
+ LEFT JOIN _v_annotations AS ia
549
+ ON ia.target_type = 'order_item'
550
+ AND ia.target_key = o.platform || ':' || o.order_no || ':' || i.item_key
551
+ LEFT JOIN _v_annotations AS pa
552
+ ON pa.target_type = 'payment' AND pa.target_key = o.payment_key
553
+ LEFT JOIN _v_beneficiary_groups AS ob
554
+ ON ob.target_type = 'order' AND ob.target_key = o.platform || ':' || o.order_no
555
+ LEFT JOIN _v_beneficiary_groups AS ib
556
+ ON ib.target_type = 'order_item'
557
+ AND ib.target_key = o.platform || ':' || o.order_no || ':' || i.item_key
558
+ LEFT JOIN _v_beneficiary_groups AS pb
559
+ ON pb.target_type = 'payment' AND pb.target_key = o.payment_key
560
+ WHERE
561
+ COALESCE(i.refunded, 0) = 0
562
+ ), weights AS (
563
+ SELECT
564
+ *,
565
+ SUM(weight) OVER (PARTITION BY platform, order_no) AS total_weight,
566
+ ROW_NUMBER() OVER (
567
+ PARTITION BY platform, order_no
568
+ ORDER BY (
569
+ weight > 0
570
+ ) DESC NULLS LAST, item_key NULLS FIRST
571
+ ) AS ordinal
572
+ FROM order_lines
573
+ ), rounded AS (
574
+ SELECT
575
+ *,
576
+ CAST(TRUNC(net_cents * CAST(1.0 AS DOUBLE PRECISION) * weight / NULLIF(total_weight, 0)) AS BIGINT) AS base_cents
577
+ FROM weights
578
+ ), allocated AS (
579
+ SELECT
580
+ *,
581
+ base_cents + CASE
582
+ WHEN ordinal = 1
583
+ THEN net_cents - SUM(base_cents) OVER (PARTITION BY platform, order_no)
584
+ ELSE 0
585
+ END AS amount_cents
586
+ FROM rounded
587
+ ), rows AS (
588
+ SELECT
589
+ CASE WHEN item_key IS NULL THEN 'order' ELSE 'order_item' END AS source_type,
590
+ platform || ':' || order_no || CASE WHEN item_key IS NULL THEN '' ELSE ':' || item_key END AS source_key,
591
+ platform,
592
+ order_no,
593
+ item_key,
594
+ occurred_at,
595
+ title,
596
+ shop_name,
597
+ category,
598
+ sensitivity,
599
+ amount_cents,
600
+ amount_basis,
601
+ refund_basis,
602
+ amount_is_estimated,
603
+ beneficiaries_json,
604
+ hidden,
605
+ CASE
606
+ WHEN title LIKE '%火车票%'
607
+ OR title LIKE '%高铁票%'
608
+ OR title LIKE '%12306%'
609
+ OR shop_name LIKE '%火车票%'
610
+ OR shop_name LIKE '%铁路%'
611
+ OR shop_name LIKE '%12306%'
612
+ OR category IN ('火车票', '车票', 'train_ticket', 'train')
613
+ THEN 1
614
+ ELSE 0
615
+ END AS is_train,
616
+ CAST(EXISTS(
617
+ SELECT
618
+ 1
619
+ FROM payment_order_links AS l
620
+ WHERE
621
+ l.order_platform = allocated.platform
622
+ AND l.order_no = allocated.order_no
623
+ AND l.deleted_at IS NULL
624
+ AND l.confidence <> 'high'
625
+ AND l.match_method <> 'manual'
626
+ ) AS INT) AS needs_match_review
627
+ FROM allocated
628
+ UNION ALL
629
+ SELECT
630
+ 'payment',
631
+ p.target_key,
632
+ p.platform,
633
+ NULL,
634
+ NULL,
635
+ p.occurred_at,
636
+ COALESCE(p.item_desc, p.counterparty, '支付'),
637
+ p.counterparty,
638
+ COALESCE(p.analysis_category, p.platform_category),
639
+ p.sensitivity,
640
+ p.net_amount_cents,
641
+ 'payment',
642
+ CASE WHEN p.effective_refund_cents > 0 THEN 'payment' ELSE 'none' END,
643
+ 0,
644
+ COALESCE(b.beneficiaries_json, '[]'),
645
+ p.hidden,
646
+ CASE
647
+ WHEN p.item_desc LIKE '%火车票%'
648
+ OR p.item_desc LIKE '%高铁票%'
649
+ OR p.item_desc LIKE '%12306%'
650
+ OR p.counterparty LIKE '%铁路%'
651
+ OR p.counterparty LIKE '%12306%'
652
+ OR p.analysis_category IN ('火车票', '车票', 'train_ticket', 'train')
653
+ THEN 1
654
+ ELSE 0
655
+ END,
656
+ CAST(EXISTS(
657
+ SELECT
658
+ 1
659
+ FROM payment_order_links AS l
660
+ WHERE
661
+ l.payment_platform = p.platform
662
+ AND l.payment_txn_id = p.txn_id
663
+ AND l.deleted_at IS NULL
664
+ AND l.confidence <> 'high'
665
+ AND l.match_method <> 'manual'
666
+ ) AS INT)
667
+ FROM v_payments AS p
668
+ LEFT JOIN _v_beneficiary_groups AS b
669
+ ON b.target_type = 'payment' AND b.target_key = p.target_key
670
+ WHERE
671
+ p.direction = 'expense'
672
+ AND p.is_refund = 0
673
+ AND p.is_transfer = 0
674
+ AND NOT EXISTS(
675
+ SELECT
676
+ 1
677
+ FROM _v_trusted_payment_orders AS l
678
+ WHERE
679
+ l.payment_platform = p.platform AND l.payment_txn_id = p.txn_id
680
+ )
681
+ )
682
+ SELECT
683
+ source_type,
684
+ source_key,
685
+ platform,
686
+ order_no,
687
+ item_key,
688
+ occurred_at,
689
+ title,
690
+ shop_name,
691
+ category,
692
+ sensitivity,
693
+ amount_cents,
694
+ amount_cents / NULLIF(100.0, 0) AS amount_yuan,
695
+ amount_basis,
696
+ refund_basis,
697
+ amount_is_estimated,
698
+ beneficiaries_json,
699
+ needs_match_review
700
+ FROM rows
701
+ WHERE
702
+ hidden = 0 AND is_train = 0 AND (
703
+ amount_cents IS NULL OR amount_cents > 0
704
+ );
705
+
706
+ /* _v_purchase_costs */
707
+ /* _v_purchase_costs */
708
+ CREATE VIEW _v_purchase_costs AS
709
+ SELECT
710
+ source_type,
711
+ source_key,
712
+ CASE WHEN needs_match_review = 0 THEN amount_cents END AS amount_cents,
713
+ CAST(amount_cents IS NULL AS INT) AS unknown_amount_count,
714
+ needs_match_review AS pending_match_count,
715
+ CAST((
716
+ amount_is_estimated = 1 AND NOT amount_cents IS NULL
717
+ ) AS INT) AS estimated_count,
718
+ sensitivity
719
+ FROM v_spending
720
+ WHERE
721
+ source_type <> 'order'
722
+ UNION ALL
723
+ SELECT
724
+ 'order',
725
+ platform || ':' || order_no,
726
+ SUM(CASE WHEN needs_match_review = 0 THEN amount_cents END),
727
+ SUM(CAST(amount_cents IS NULL AS INT)),
728
+ SUM(needs_match_review),
729
+ SUM(CAST(amount_is_estimated = 1 AND NOT amount_cents IS NULL AS INT)),
730
+ CASE
731
+ WHEN SUM(CAST(sensitivity = 'intimate' AS INT)) > 0
732
+ THEN 'intimate'
733
+ WHEN SUM(CAST(sensitivity = 'private' AS INT)) > 0
734
+ THEN 'private'
735
+ ELSE 'normal'
736
+ END
737
+ FROM v_spending
738
+ WHERE
739
+ NOT order_no IS NULL
740
+ GROUP BY
741
+ platform,
742
+ order_no;
743
+
744
+ /* _v_spending_share_rows */
745
+ /* _v_spending_share_rows */
746
+ CREATE VIEW _v_spending_share_rows AS
747
+ WITH shares AS (
748
+ SELECT
749
+ s.source_type,
750
+ s.source_key,
751
+ s.amount_cents,
752
+ JSON_EXTRACT_PATH_TEXT(j."value", 'person_id') AS person_id,
753
+ CAST(JSON_EXTRACT_PATH_TEXT(j."value", 'share_ratio') AS DOUBLE PRECISION) AS ratio
754
+ FROM v_spending AS s
755
+ CROSS JOIN JSON_EACH(s.beneficiaries_json) AS j
756
+ ), checked AS (
757
+ SELECT
758
+ *,
759
+ SUM(ratio) OVER (PARTITION BY source_type, source_key) AS total_ratio,
760
+ MIN(CASE WHEN ratio IS NOT NULL AND ratio BETWEEN 0 AND 1 THEN 1 ELSE 0 END) OVER (PARTITION BY source_type, source_key) AS ratios_valid,
761
+ ROW_NUMBER() OVER (
762
+ PARTITION BY source_type, source_key
763
+ ORDER BY (
764
+ ratio > 0
765
+ ) DESC NULLS LAST, person_id NULLS FIRST
766
+ ) AS ordinal
767
+ FROM shares
768
+ ), rounded AS (
769
+ SELECT
770
+ *,
771
+ CAST(TRUNC(amount_cents * ratio) AS BIGINT) AS base_cents
772
+ FROM checked
773
+ )
774
+ SELECT
775
+ *,
776
+ CASE
777
+ WHEN ratios_valid = 1 AND total_ratio <= 1.000000001
778
+ THEN base_cents + CASE
779
+ WHEN ordinal = 1
780
+ THEN CAST(TRUNC(ROUND(CAST(amount_cents * LEAST(total_ratio, 1) AS DECIMAL))) AS BIGINT) - SUM(base_cents) OVER (PARTITION BY source_type, source_key)
781
+ ELSE 0
782
+ END
783
+ END AS allocated_cents,
784
+ CASE WHEN ratios_valid = 1 AND total_ratio <= 1.000000001 THEN 0 ELSE 1 END AS invalid_shares
785
+ FROM rounded;
786
+
787
+ /* _v_spending_quality */
788
+ /* _v_spending_quality */
789
+ CREATE VIEW _v_spending_quality AS
790
+ WITH allocations AS (
791
+ SELECT
792
+ source_type,
793
+ source_key,
794
+ SUM(allocated_cents) AS allocated_cents,
795
+ MAX(invalid_shares) AS invalid_shares
796
+ FROM _v_spending_share_rows
797
+ GROUP BY
798
+ source_type,
799
+ source_key
800
+ )
801
+ SELECT
802
+ s.*,
803
+ COALESCE(a.invalid_shares, 0) AS invalid_shares,
804
+ CASE
805
+ WHEN s.amount_cents IS NULL
806
+ THEN NULL
807
+ ELSE s.amount_cents - COALESCE(a.allocated_cents, 0)
808
+ END AS unassigned_cents
809
+ FROM v_spending AS s
810
+ LEFT JOIN allocations AS a
811
+ ON a.source_type = s.source_type AND a.source_key = s.source_key;
812
+
813
+ /* v_messages */
814
+ /* v_messages */
815
+ CREATE VIEW v_messages AS
816
+ WITH matched AS (
817
+ SELECT
818
+ m.msg_id,
819
+ MIN(i.person_id) AS person_id,
820
+ COUNT(DISTINCT i.person_id) AS persons
821
+ FROM wechat_messages AS m
822
+ JOIN wechat_sessions AS s
823
+ ON s.session_key = m.session_key
824
+ JOIN identities AS i
825
+ ON i.source = s.platform
826
+ AND i.match_kind = 'account_id'
827
+ AND VAULT_IDENTITY_MATCH(i.match_mode, i."value", m.sender_platform_id) = 1
828
+ AND i.deleted_at IS NULL
829
+ AND (
830
+ i.valid_from IS NULL OR i.valid_from <= SUBSTRING(m.sent_at FROM 1 FOR 10)
831
+ )
832
+ AND (
833
+ i.valid_to IS NULL OR i.valid_to >= SUBSTRING(m.sent_at FROM 1 FOR 10)
834
+ )
835
+ JOIN persons AS p
836
+ ON p.id = i.person_id AND p.deleted_at IS NULL
837
+ GROUP BY
838
+ m.msg_id
839
+ )
840
+ SELECT
841
+ m.*,
842
+ s.name AS session_name,
843
+ s.session_type,
844
+ CASE WHEN i.persons = 1 THEN i.person_id END AS person_id,
845
+ CASE WHEN i.persons = 1 THEN p.display_name END AS person_name,
846
+ COALESCE(CAST(i.persons > 1 AS INT), 0) AS identity_ambiguous,
847
+ COALESCE(a.hidden, 0) AS hidden,
848
+ COALESCE(a.sensitivity, 'normal') AS sensitivity
849
+ FROM wechat_messages AS m
850
+ JOIN wechat_sessions AS s
851
+ ON s.session_key = m.session_key
852
+ LEFT JOIN matched AS i
853
+ ON i.msg_id = m.msg_id
854
+ LEFT JOIN persons AS p
855
+ ON p.id = i.person_id
856
+ LEFT JOIN _v_annotations AS a
857
+ ON a.target_type = 'message' AND a.target_key = m.msg_id;
858
+
859
+ /* _v_us_messages */
860
+ /* _v_us_messages */
861
+ CREATE VIEW _v_us_messages AS
862
+ WITH messages AS MATERIALIZED (
863
+ SELECT
864
+ msg_id,
865
+ session_key,
866
+ sent_at,
867
+ sent_ts,
868
+ person_id,
869
+ hidden,
870
+ msg_type
871
+ FROM v_messages
872
+ WHERE
873
+ session_type = 'private' AND NOT msg_type IN (80, 81)
874
+ ), session_days AS (
875
+ SELECT DISTINCT
876
+ m.session_key,
877
+ SUBSTRING(m.sent_at FROM 1 FOR 10) AS sent_on,
878
+ s.platform,
879
+ s.owner_platform_id,
880
+ s.peer_platform_id
881
+ FROM messages AS m
882
+ JOIN wechat_sessions AS s
883
+ ON s.session_key = m.session_key
884
+ ), endpoints AS (
885
+ SELECT
886
+ session_key,
887
+ sent_on,
888
+ platform,
889
+ 'owner' AS endpoint,
890
+ owner_platform_id AS "value"
891
+ FROM session_days
892
+ UNION ALL
893
+ SELECT
894
+ session_key,
895
+ sent_on,
896
+ platform,
897
+ 'peer',
898
+ peer_platform_id
899
+ FROM session_days
900
+ ), matched AS (
901
+ SELECT
902
+ e.session_key,
903
+ e.sent_on,
904
+ e.endpoint,
905
+ MIN(i.person_id) AS person_id,
906
+ COUNT(DISTINCT i.person_id) AS matches
907
+ FROM endpoints AS e
908
+ JOIN identities AS i
909
+ ON i.source = e.platform
910
+ AND i.match_kind = 'account_id'
911
+ AND i.deleted_at IS NULL
912
+ AND VAULT_IDENTITY_MATCH(i.match_mode, i."value", e."value") = 1
913
+ AND (
914
+ i.valid_from IS NULL OR i.valid_from <= e.sent_on
915
+ )
916
+ AND (
917
+ i.valid_to IS NULL OR i.valid_to >= e.sent_on
918
+ )
919
+ JOIN persons AS p
920
+ ON p.id = i.person_id AND p.deleted_at IS NULL
921
+ GROUP BY
922
+ e.session_key,
923
+ e.sent_on,
924
+ e.endpoint
925
+ ), pairs AS (
926
+ SELECT
927
+ o.session_key,
928
+ o.sent_on
929
+ FROM matched AS o
930
+ JOIN matched AS peer
931
+ ON peer.session_key = o.session_key
932
+ AND peer.sent_on = o.sent_on
933
+ AND peer.endpoint = 'peer'
934
+ AND peer.matches = 1
935
+ JOIN _v_module_people AS op
936
+ ON op.id = o.person_id
937
+ JOIN _v_module_people AS pp
938
+ ON pp.id = peer.person_id
939
+ WHERE
940
+ o.endpoint = 'owner' AND o.matches = 1 AND op.module <> pp.module
941
+ )
942
+ SELECT
943
+ m.*,
944
+ CASE WHEN m.hidden = 0 THEN p.module END AS sender_module
945
+ FROM messages AS m
946
+ JOIN pairs AS pair
947
+ ON pair.session_key = m.session_key
948
+ AND pair.sent_on = SUBSTRING(m.sent_at FROM 1 FOR 10)
949
+ LEFT JOIN _v_module_people AS p
950
+ ON p.id = m.person_id;
951
+
952
+ /* _v_us_chat_turns */
953
+ /* _v_us_chat_turns */
954
+ CREATE VIEW _v_us_chat_turns AS
955
+ WITH messages AS MATERIALIZED (
956
+ SELECT
957
+ *
958
+ FROM _v_us_messages
959
+ ), ties AS (
960
+ SELECT
961
+ session_key,
962
+ sent_ts,
963
+ CAST((
964
+ COUNT(DISTINCT sender_module) > 1 OR COUNT(*) <> COUNT(sender_module)
965
+ ) AS INT) AS ambiguous
966
+ FROM messages
967
+ GROUP BY
968
+ session_key,
969
+ sent_ts
970
+ ), ordered AS (
971
+ SELECT
972
+ m.*,
973
+ t.ambiguous,
974
+ LAG(sender_module) OVER history AS previous_sender,
975
+ LAG(t.ambiguous) OVER history AS previous_ambiguous
976
+ FROM messages AS m
977
+ JOIN ties AS t
978
+ ON t.session_key = m.session_key AND t.sent_ts = m.sent_ts
979
+ WINDOW history AS (PARTITION BY m.session_key ORDER BY m.sent_ts NULLS FIRST, m.msg_id NULLS FIRST)
980
+ ), grouped AS (
981
+ SELECT
982
+ *,
983
+ SUM(
984
+ CASE
985
+ WHEN NOT sender_module IS NULL
986
+ AND sender_module = previous_sender
987
+ AND ambiguous = 0
988
+ AND previous_ambiguous = 0
989
+ THEN 0
990
+ ELSE 1
991
+ END
992
+ ) OVER (
993
+ PARTITION BY session_key
994
+ ORDER BY sent_ts NULLS FIRST, msg_id NULLS FIRST
995
+ ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
996
+ ) AS turn_no
997
+ FROM ordered
998
+ )
999
+ SELECT
1000
+ session_key,
1001
+ turn_no,
1002
+ sender_module,
1003
+ MIN(sent_ts) AS start_ts,
1004
+ MAX(sent_ts) AS end_ts,
1005
+ MIN(sent_at) AS started_at,
1006
+ MAX(ambiguous) AS timing_ambiguous
1007
+ FROM grouped
1008
+ GROUP BY
1009
+ session_key,
1010
+ turn_no,
1011
+ sender_module;
1012
+
1013
+ /* _v_us_events */
1014
+ /* _v_us_events */
1015
+ CREATE VIEW _v_us_events AS
1016
+ SELECT
1017
+ e.*,
1018
+ COALESCE(a.sensitivity, e.sensitivity) AS effective_sensitivity
1019
+ FROM events AS e
1020
+ LEFT JOIN _v_annotations AS a
1021
+ ON a.target_type = 'events' AND a.target_key = e.id
1022
+ WHERE
1023
+ e.deleted_at IS NULL
1024
+ AND e.status = 'confirmed'
1025
+ AND COALESCE(a.hidden, 0) = 0
1026
+ AND EXISTS(
1027
+ SELECT
1028
+ 1
1029
+ FROM _v_module_people
1030
+ WHERE
1031
+ module = 'me'
1032
+ )
1033
+ AND EXISTS(
1034
+ SELECT
1035
+ 1
1036
+ FROM _v_module_people
1037
+ WHERE
1038
+ module = 'her'
1039
+ )
1040
+ AND (
1041
+ NOT EXISTS(
1042
+ SELECT
1043
+ 1
1044
+ FROM event_participants AS p
1045
+ WHERE
1046
+ p.event_id = e.id
1047
+ )
1048
+ OR (
1049
+ EXISTS(
1050
+ SELECT
1051
+ 1
1052
+ FROM event_participants AS p
1053
+ JOIN _v_module_people AS me
1054
+ ON me.id = p.person_id AND me.module = 'me'
1055
+ WHERE
1056
+ p.event_id = e.id AND p.deleted_at IS NULL
1057
+ )
1058
+ AND EXISTS(
1059
+ SELECT
1060
+ 1
1061
+ FROM event_participants AS p
1062
+ JOIN _v_module_people AS her
1063
+ ON her.id = p.person_id AND her.module = 'her'
1064
+ WHERE
1065
+ p.event_id = e.id AND p.deleted_at IS NULL
1066
+ )
1067
+ )
1068
+ );
1069
+
1070
+ /* v_meet_days */
1071
+ /* v_meet_days */
1072
+ CREATE VIEW v_meet_days AS
1073
+ WITH RECURSIVE days(event_id, meet_on, end_on) AS (
1074
+ SELECT
1075
+ e.id,
1076
+ DATE(e.start_at),
1077
+ DATE(COALESCE(e.end_at, e.start_at))
1078
+ FROM events AS e
1079
+ LEFT JOIN _v_annotations AS a
1080
+ ON a.target_type = 'events' AND a.target_key = e.id
1081
+ WHERE
1082
+ e.kind = 'meetup'
1083
+ AND e.deleted_at IS NULL
1084
+ AND e.status = 'confirmed'
1085
+ AND COALESCE(a.hidden, 0) = 0
1086
+ UNION ALL
1087
+ SELECT
1088
+ event_id,
1089
+ DATE(meet_on, '+1 day'),
1090
+ end_on
1091
+ FROM days
1092
+ WHERE
1093
+ meet_on < end_on
1094
+ )
1095
+ SELECT
1096
+ meet_on,
1097
+ COUNT(*) AS event_count,
1098
+ CAST(JSON_AGG(event_id) AS TEXT) AS event_ids_json
1099
+ FROM days
1100
+ GROUP BY
1101
+ meet_on;
1102
+
1103
+ /* v_us_meetups */
1104
+ /* v_us_meetups */
1105
+ CREATE VIEW v_us_meetups AS
1106
+ SELECT
1107
+ *,
1108
+ CAST(TRUNC(JULIANDAY(DATE(COALESCE(end_at, start_at))) - JULIANDAY(DATE(start_at))) AS BIGINT) + 1 AS calendar_days,
1109
+ CASE WHEN DATE(start_at) <= VAULT_TODAY() THEN 1 ELSE 0 END AS has_started
1110
+ FROM _v_us_events
1111
+ WHERE
1112
+ kind = 'meetup';
1113
+
1114
+ /* _v_us_meet_days */
1115
+ /* _v_us_meet_days */
1116
+ CREATE VIEW _v_us_meet_days AS
1117
+ SELECT
1118
+ d.meet_on,
1119
+ COUNT(*) AS event_count
1120
+ FROM v_meet_days AS d
1121
+ CROSS JOIN JSON_EACH(d.event_ids_json) AS j
1122
+ JOIN v_us_meetups AS m
1123
+ ON m.id = j."value"
1124
+ GROUP BY
1125
+ d.meet_on;
1126
+
1127
+ /* _v_us_partner_start */
1128
+ /* _v_us_partner_start */
1129
+ CREATE VIEW _v_us_partner_start AS
1130
+ WITH relations_current AS (
1131
+ SELECT
1132
+ r.*
1133
+ FROM relations AS r
1134
+ JOIN _v_module_people AS a
1135
+ ON a.id = r.from_person_id
1136
+ JOIN _v_module_people AS b
1137
+ ON b.id = r.to_person_id
1138
+ LEFT JOIN _v_annotations AS label
1139
+ ON label.target_type = 'relations' AND label.target_key = r.id
1140
+ WHERE
1141
+ r.relation = 'partner'
1142
+ AND a.module <> b.module
1143
+ AND r.status = 'confirmed'
1144
+ AND r.deleted_at IS NULL
1145
+ AND COALESCE(label.hidden, 0) = 0
1146
+ AND (
1147
+ r.until_on IS NULL OR r.until_on >= VAULT_TODAY()
1148
+ )
1149
+ AND (
1150
+ r.since_on IS NULL OR r.since_on <= VAULT_TODAY()
1151
+ )
1152
+ )
1153
+ SELECT
1154
+ CASE
1155
+ WHEN COUNT(DISTINCT since_on) = 1 AND DATE(MIN(since_on)) = MIN(since_on)
1156
+ THEN MIN(since_on)
1157
+ END AS since_on,
1158
+ CASE
1159
+ WHEN COUNT(DISTINCT since_on) > 1
1160
+ THEN 'conflicting_start_dates'
1161
+ WHEN COUNT(DISTINCT since_on) = 1 AND DATE(MIN(since_on)) = MIN(since_on)
1162
+ THEN 'confirmed_partner_relation'
1163
+ ELSE 'no_confirmed_start'
1164
+ END AS basis
1165
+ FROM relations_current;
1166
+
1167
+ /* _v_us_reply_samples */
1168
+ /* _v_us_reply_samples */
1169
+ CREATE VIEW _v_us_reply_samples AS
1170
+ WITH preceding AS (
1171
+ SELECT
1172
+ *,
1173
+ LAG(sender_module) OVER history AS previous_sender,
1174
+ LAG(end_ts) OVER history AS previous_end_ts,
1175
+ LAG(timing_ambiguous) OVER history AS previous_ambiguous
1176
+ FROM _v_us_chat_turns
1177
+ WINDOW history AS (PARTITION BY session_key ORDER BY turn_no NULLS FIRST)
1178
+ ), samples AS (
1179
+ SELECT
1180
+ *,
1181
+ start_ts - previous_end_ts AS reply_seconds,
1182
+ COALESCE(
1183
+ (
1184
+ SELECT
1185
+ CASE
1186
+ WHEN JSON_TYPE(value_json) = 'integer'
1187
+ THEN GREATEST(1, LEAST(604800, CAST(value_json AS BIGINT)))
1188
+ END
1189
+ FROM settings
1190
+ WHERE
1191
+ key = 'reply_max_gap_seconds'
1192
+ ),
1193
+ 86400
1194
+ ) AS max_gap
1195
+ FROM preceding
1196
+ WHERE
1197
+ NOT sender_module IS NULL
1198
+ AND NOT previous_sender IS NULL
1199
+ AND sender_module <> previous_sender
1200
+ AND timing_ambiguous = 0
1201
+ AND previous_ambiguous = 0
1202
+ )
1203
+ SELECT
1204
+ *,
1205
+ CASE WHEN reply_seconds BETWEEN 0 AND max_gap THEN 1 ELSE 0 END AS usable
1206
+ FROM samples;
1207
+
1208
+ /* _v_us_reply_daily */
1209
+ /* _v_us_reply_daily */
1210
+ CREATE VIEW _v_us_reply_daily AS
1211
+ WITH usable AS (
1212
+ SELECT
1213
+ *,
1214
+ SUBSTRING(started_at FROM 1 FOR 10) AS reply_on,
1215
+ ROW_NUMBER() OVER (
1216
+ PARTITION BY SUBSTRING(started_at FROM 1 FOR 10), sender_module
1217
+ ORDER BY reply_seconds NULLS FIRST, session_key NULLS FIRST, turn_no NULLS FIRST
1218
+ ) AS position,
1219
+ COUNT(*) OVER (PARTITION BY SUBSTRING(started_at FROM 1 FOR 10), sender_module) AS samples
1220
+ FROM _v_us_reply_samples
1221
+ WHERE
1222
+ usable = 1
1223
+ ), stats AS (
1224
+ SELECT
1225
+ reply_on,
1226
+ sender_module,
1227
+ COUNT(*) AS reply_count,
1228
+ AVG(reply_seconds) AS avg_reply_seconds,
1229
+ AVG(
1230
+ CASE
1231
+ WHEN position IN ((
1232
+ samples + 1
1233
+ ) / NULLIF(2, 0), (
1234
+ samples + 2
1235
+ ) / NULLIF(2, 0))
1236
+ THEN reply_seconds
1237
+ END
1238
+ ) AS median_reply_seconds,
1239
+ MIN(reply_seconds) AS min_reply_seconds,
1240
+ MAX(reply_seconds) AS max_reply_seconds
1241
+ FROM usable
1242
+ GROUP BY
1243
+ reply_on,
1244
+ sender_module
1245
+ ), days AS (
1246
+ SELECT
1247
+ SUBSTRING(started_at FROM 1 FOR 10) AS reply_on,
1248
+ sender_module,
1249
+ SUM(CAST(usable = 0 AS INT)) AS long_gap_count
1250
+ FROM _v_us_reply_samples
1251
+ GROUP BY
1252
+ SUBSTRING(started_at FROM 1 FOR 10),
1253
+ sender_module
1254
+ )
1255
+ SELECT
1256
+ d.reply_on,
1257
+ d.sender_module,
1258
+ COALESCE(s.reply_count, 0) AS reply_count,
1259
+ s.avg_reply_seconds,
1260
+ s.median_reply_seconds,
1261
+ s.min_reply_seconds,
1262
+ s.max_reply_seconds,
1263
+ d.long_gap_count
1264
+ FROM days AS d
1265
+ LEFT JOIN stats AS s
1266
+ ON s.reply_on = d.reply_on AND s.sender_module = d.sender_module;
1267
+
1268
+ /* v_us_conflicts */
1269
+ /* v_us_conflicts */
1270
+ CREATE VIEW v_us_conflicts AS
1271
+ SELECT
1272
+ *
1273
+ FROM _v_us_events
1274
+ WHERE
1275
+ kind = 'conflict';
1276
+
1277
+ /* q_conflicts_total */
1278
+ /* q_conflicts_total */
1279
+ CREATE VIEW q_conflicts_total AS
1280
+ SELECT
1281
+ COUNT(*) AS "value"
1282
+ FROM v_us_conflicts
1283
+ WHERE
1284
+ DATE(start_at) <= VAULT_TODAY();
1285
+
1286
+ /* q_days_together */
1287
+ /* q_days_together */
1288
+ CREATE VIEW q_days_together AS
1289
+ SELECT
1290
+ CASE
1291
+ WHEN NOT since_on IS NULL
1292
+ THEN CAST(TRUNC(JULIANDAY(VAULT_TODAY()) - JULIANDAY(since_on)) AS BIGINT) + 1
1293
+ END AS "value",
1294
+ since_on,
1295
+ basis
1296
+ FROM _v_us_partner_start;
1297
+
1298
+ /* q_first_meet_on */
1299
+ /* q_first_meet_on */
1300
+ CREATE VIEW q_first_meet_on AS
1301
+ SELECT
1302
+ MIN(DATE(start_at)) AS "value"
1303
+ FROM v_us_meetups
1304
+ WHERE
1305
+ has_started = 1;
1306
+
1307
+ /* v_us_gifts */
1308
+ /* v_us_gifts */
1309
+ CREATE VIEW v_us_gifts AS
1310
+ SELECT
1311
+ 'gift_ideas' AS source_type,
1312
+ g.id AS source_key,
1313
+ g.title,
1314
+ g.for_person_id,
1315
+ g.given_event_id AS event_id,
1316
+ e.start_at AS given_at,
1317
+ c.amount_cents,
1318
+ c.amount_cents / NULLIF(100.0, 0) AS amount_yuan,
1319
+ g.budget_cents,
1320
+ COALESCE(c.unknown_amount_count, 1) AS unknown_amount_count,
1321
+ COALESCE(c.pending_match_count, 0) AS pending_match_count,
1322
+ COALESCE(c.estimated_count, 0) AS estimated_count,
1323
+ CASE
1324
+ WHEN COALESCE(a.sensitivity, g.sensitivity) = 'intimate'
1325
+ OR e.effective_sensitivity = 'intimate'
1326
+ OR c.sensitivity = 'intimate'
1327
+ THEN 'intimate'
1328
+ WHEN NOT c.source_key IS NULL
1329
+ OR COALESCE(a.sensitivity, g.sensitivity) = 'private'
1330
+ OR e.effective_sensitivity = 'private'
1331
+ THEN 'private'
1332
+ ELSE 'normal'
1333
+ END AS sensitivity
1334
+ FROM gift_ideas AS g
1335
+ JOIN _v_module_people AS recipient
1336
+ ON recipient.id = g.for_person_id
1337
+ LEFT JOIN _v_us_events AS e
1338
+ ON e.id = g.given_event_id
1339
+ LEFT JOIN _v_purchase_costs AS c
1340
+ ON c.source_type = g.purchase_type AND c.source_key = g.purchase_key
1341
+ LEFT JOIN _v_annotations AS a
1342
+ ON a.target_type = 'gift_ideas' AND a.target_key = g.id
1343
+ WHERE
1344
+ g.deleted_at IS NULL
1345
+ AND g.status = 'confirmed'
1346
+ AND g.idea_state = 'given'
1347
+ AND COALESCE(a.hidden, 0) = 0
1348
+ AND (
1349
+ g.given_event_id IS NULL OR NOT e.id IS NULL
1350
+ )
1351
+ UNION ALL
1352
+ SELECT
1353
+ 'events',
1354
+ e.id,
1355
+ e.title,
1356
+ NULL,
1357
+ e.id,
1358
+ e.start_at,
1359
+ NULL,
1360
+ NULL,
1361
+ NULL,
1362
+ 1,
1363
+ 0,
1364
+ 0,
1365
+ e.effective_sensitivity
1366
+ FROM _v_us_events AS e
1367
+ WHERE
1368
+ e.kind = 'gift'
1369
+ AND NOT EXISTS(
1370
+ SELECT
1371
+ 1
1372
+ FROM gift_ideas AS g
1373
+ WHERE
1374
+ g.given_event_id = e.id AND g.idea_state = 'given'
1375
+ );
1376
+
1377
+ /* q_gifts_given_total */
1378
+ /* q_gifts_given_total */
1379
+ CREATE VIEW q_gifts_given_total AS
1380
+ SELECT
1381
+ COUNT(*) AS "value",
1382
+ COALESCE(SUM(CAST(given_at IS NULL AS INT)), 0) AS undated_count
1383
+ FROM v_us_gifts;
1384
+
1385
+ /* v_us_interaction_daily */
1386
+ /* v_us_interaction_daily */
1387
+ CREATE VIEW v_us_interaction_daily AS
1388
+ WITH messages AS MATERIALIZED (
1389
+ SELECT
1390
+ *
1391
+ FROM _v_us_messages
1392
+ WHERE
1393
+ hidden = 0
1394
+ ), days AS (
1395
+ SELECT
1396
+ SUBSTRING(sent_at FROM 1 FOR 10) AS chat_on,
1397
+ COUNT(*) AS message_count,
1398
+ SUM(CAST(sender_module = 'me' AS INT)) AS me_count,
1399
+ SUM(CAST(sender_module = 'her' AS INT)) AS her_count,
1400
+ SUM(CAST(sender_module IS NULL AS INT)) AS unidentified_count,
1401
+ MIN(sent_ts) AS first_ts,
1402
+ COUNT(DISTINCT session_key) AS session_count
1403
+ FROM messages
1404
+ GROUP BY
1405
+ SUBSTRING(sent_at FROM 1 FOR 10)
1406
+ ), first_senders AS (
1407
+ SELECT
1408
+ d.chat_on,
1409
+ CASE
1410
+ WHEN COUNT(DISTINCT m.sender_module) = 1 AND COUNT(*) = COUNT(m.sender_module)
1411
+ THEN MIN(m.person_id)
1412
+ END AS first_sender_id
1413
+ FROM days AS d
1414
+ JOIN messages AS m
1415
+ ON SUBSTRING(m.sent_at FROM 1 FOR 10) = d.chat_on AND m.sent_ts = d.first_ts
1416
+ GROUP BY
1417
+ d.chat_on
1418
+ )
1419
+ SELECT
1420
+ d.chat_on,
1421
+ d.message_count,
1422
+ COALESCE(d.me_count, 0) AS me_count,
1423
+ COALESCE(d.her_count, 0) AS her_count,
1424
+ d.unidentified_count,
1425
+ d.session_count,
1426
+ f.first_sender_id,
1427
+ COALESCE(me.reply_count, 0) AS me_reply_count,
1428
+ me.avg_reply_seconds AS me_avg_reply_seconds,
1429
+ COALESCE(her.reply_count, 0) AS her_reply_count,
1430
+ her.avg_reply_seconds AS her_avg_reply_seconds,
1431
+ COALESCE(me.long_gap_count, 0) + COALESCE(her.long_gap_count, 0) AS long_gap_count
1432
+ FROM days AS d
1433
+ JOIN first_senders AS f
1434
+ ON f.chat_on = d.chat_on
1435
+ LEFT JOIN _v_us_reply_daily AS me
1436
+ ON me.reply_on = d.chat_on AND me.sender_module = 'me'
1437
+ LEFT JOIN _v_us_reply_daily AS her
1438
+ ON her.reply_on = d.chat_on AND her.sender_module = 'her';
1439
+
1440
+ /* q_interaction_daily */
1441
+ /* q_interaction_daily */
1442
+ CREATE VIEW q_interaction_daily AS
1443
+ SELECT
1444
+ *
1445
+ FROM v_us_interaction_daily;
1446
+
1447
+ /* q_last_meet_on */
1448
+ /* q_last_meet_on */
1449
+ CREATE VIEW q_last_meet_on AS
1450
+ SELECT
1451
+ MAX(meet_on) AS "value"
1452
+ FROM _v_us_meet_days
1453
+ WHERE
1454
+ meet_on <= VAULT_TODAY();
1455
+
1456
+ /* q_meet_days_total */
1457
+ /* q_meet_days_total */
1458
+ CREATE VIEW q_meet_days_total AS
1459
+ SELECT
1460
+ COUNT(*) AS "value"
1461
+ FROM _v_us_meet_days
1462
+ WHERE
1463
+ meet_on <= VAULT_TODAY();
1464
+
1465
+ /* q_meetups_total */
1466
+ /* q_meetups_total */
1467
+ CREATE VIEW q_meetups_total AS
1468
+ SELECT
1469
+ COUNT(*) AS "value"
1470
+ FROM v_us_meetups
1471
+ WHERE
1472
+ has_started = 1;
1473
+
1474
+ /* v_money_flow */
1475
+ /* v_money_flow */
1476
+ CREATE VIEW v_money_flow AS
1477
+ WITH participants AS (
1478
+ SELECT
1479
+ p.*,
1480
+ me.id AS self_id,
1481
+ partner.id AS partner_id,
1482
+ CASE WHEN p.direction = 'expense' THEN me.id ELSE partner.id END AS from_person_id,
1483
+ CASE WHEN p.direction = 'expense' THEN partner.id ELSE me.id END AS to_person_id
1484
+ FROM v_payments AS p
1485
+ JOIN persons AS me
1486
+ ON me.is_self = 1 AND me.deleted_at IS NULL
1487
+ JOIN persons AS partner
1488
+ ON partner.is_default = 1 AND partner.deleted_at IS NULL
1489
+ WHERE
1490
+ p.counterparty_person_id = partner.id
1491
+ AND p.is_transfer = 1
1492
+ AND p.is_refund = 0
1493
+ AND p.direction IN ('expense', 'income')
1494
+ AND p.hidden = 0
1495
+ AND p.net_amount_cents > 0
1496
+ )
1497
+ SELECT
1498
+ SUBSTRING(occurred_at FROM 1 FOR 10) AS flow_on,
1499
+ from_person_id,
1500
+ to_person_id,
1501
+ COUNT(*) AS transfer_count,
1502
+ SUM(amount_cents) AS gross_cents,
1503
+ SUM(effective_refund_cents) AS refund_cents,
1504
+ SUM(net_amount_cents) AS amount_cents,
1505
+ SUM(net_amount_cents) / NULLIF(100.0, 0) AS amount_yuan
1506
+ FROM participants
1507
+ GROUP BY
1508
+ SUBSTRING(occurred_at FROM 1 FOR 10),
1509
+ from_person_id,
1510
+ to_person_id;
1511
+
1512
+ /* v_us_money_flow */
1513
+ /* v_us_money_flow */
1514
+ CREATE VIEW v_us_money_flow AS
1515
+ SELECT
1516
+ *
1517
+ FROM v_money_flow;
1518
+
1519
+ /* q_money_received_total */
1520
+ /* q_money_received_total */
1521
+ CREATE VIEW q_money_received_total AS
1522
+ SELECT
1523
+ COALESCE(SUM(f.amount_cents), 0) / NULLIF(100.0, 0) AS "value",
1524
+ COALESCE(SUM(f.amount_cents), 0) AS amount_cents
1525
+ FROM v_us_money_flow AS f
1526
+ JOIN _v_module_people AS p
1527
+ ON p.id = f.to_person_id AND p.module = 'me';
1528
+
1529
+ /* q_money_sent_total */
1530
+ /* q_money_sent_total */
1531
+ CREATE VIEW q_money_sent_total AS
1532
+ SELECT
1533
+ COALESCE(SUM(f.amount_cents), 0) / NULLIF(100.0, 0) AS "value",
1534
+ COALESCE(SUM(f.amount_cents), 0) AS amount_cents
1535
+ FROM v_us_money_flow AS f
1536
+ JOIN _v_module_people AS p
1537
+ ON p.id = f.from_person_id AND p.module = 'me';
1538
+
1539
+ /* q_next_meetup_on */
1540
+ /* q_next_meetup_on */
1541
+ CREATE VIEW q_next_meetup_on AS
1542
+ SELECT
1543
+ MIN(DATE(start_at)) AS "value"
1544
+ FROM v_us_meetups
1545
+ WHERE
1546
+ DATE(start_at) >= VAULT_TODAY();
1547
+
1548
+ /* v_promises */
1549
+ /* v_promises */
1550
+ CREATE VIEW v_promises AS
1551
+ WITH checks AS (
1552
+ SELECT
1553
+ c.promise_id,
1554
+ SUM(CAST(c.result = 'broken' AS INT)) AS broken_count,
1555
+ COUNT(*) AS check_count,
1556
+ MAX(c.checked_on) AS last_checked_on
1557
+ FROM promise_checks AS c
1558
+ LEFT JOIN _v_annotations AS a
1559
+ ON a.target_type = 'promise_checks' AND a.target_key = c.id
1560
+ WHERE
1561
+ c.deleted_at IS NULL AND c.status = 'confirmed' AND COALESCE(a.hidden, 0) = 0
1562
+ GROUP BY
1563
+ c.promise_id
1564
+ )
1565
+ SELECT
1566
+ p.*,
1567
+ COALESCE(c.broken_count, 0) AS broken_count,
1568
+ COALESCE(c.check_count, 0) AS check_count,
1569
+ c.last_checked_on,
1570
+ CASE
1571
+ WHEN p.due_on < VAULT_TODAY() AND p.fulfillment IN ('active', 'pending')
1572
+ THEN 1
1573
+ ELSE 0
1574
+ END AS overdue
1575
+ FROM promises AS p
1576
+ LEFT JOIN checks AS c
1577
+ ON c.promise_id = p.id
1578
+ LEFT JOIN _v_annotations AS a
1579
+ ON a.target_type = 'promises' AND a.target_key = p.id
1580
+ WHERE
1581
+ p.deleted_at IS NULL AND p.status = 'confirmed' AND COALESCE(a.hidden, 0) = 0;
1582
+
1583
+ /* v_me_promises */
1584
+ /* v_me_promises */
1585
+ CREATE VIEW v_me_promises AS
1586
+ SELECT
1587
+ p.*
1588
+ FROM v_promises AS p
1589
+ JOIN _v_module_people AS me
1590
+ ON me.module = 'me'
1591
+ WHERE
1592
+ p.promiser_id = me.id OR (
1593
+ p.is_mutual = 1 AND p.recipient_id = me.id
1594
+ );
1595
+
1596
+ /* q_promises_open */
1597
+ /* q_promises_open */
1598
+ CREATE VIEW q_promises_open AS
1599
+ SELECT
1600
+ COUNT(*) AS "value",
1601
+ COALESCE(SUM(overdue), 0) AS overdue_count
1602
+ FROM v_me_promises
1603
+ WHERE
1604
+ fulfillment IN ('active', 'pending');
1605
+
1606
+ /* v_us_spending_by_beneficiary */
1607
+ /* v_us_spending_by_beneficiary */
1608
+ CREATE VIEW v_us_spending_by_beneficiary AS
1609
+ SELECT
1610
+ s.source_type,
1611
+ s.source_key,
1612
+ s.platform,
1613
+ s.occurred_at,
1614
+ s.category,
1615
+ s.title,
1616
+ s.sensitivity,
1617
+ b.person_id,
1618
+ b.ratio AS share_ratio,
1619
+ b.allocated_cents AS amount_cents,
1620
+ b.allocated_cents / NULLIF(100.0, 0) AS amount_yuan,
1621
+ s.amount_is_estimated,
1622
+ s.needs_match_review
1623
+ FROM v_spending AS s
1624
+ JOIN _v_spending_share_rows AS b
1625
+ ON b.source_type = s.source_type AND b.source_key = s.source_key
1626
+ WHERE
1627
+ b.invalid_shares = 0 AND b.ratio > 0;
1628
+
1629
+ /* q_spend_on_her_yearly */
1630
+ /* q_spend_on_her_yearly */
1631
+ CREATE VIEW q_spend_on_her_yearly AS
1632
+ WITH years AS (
1633
+ SELECT DISTINCT
1634
+ SUBSTRING(occurred_at FROM 1 FOR 4) AS year
1635
+ FROM v_spending
1636
+ ), amounts AS (
1637
+ SELECT
1638
+ SUBSTRING(s.occurred_at FROM 1 FOR 4) AS year,
1639
+ SUM(s.amount_cents) AS amount_cents
1640
+ FROM v_us_spending_by_beneficiary AS s
1641
+ JOIN _v_module_people AS p
1642
+ ON p.id = s.person_id AND p.module = 'her'
1643
+ WHERE
1644
+ s.needs_match_review = 0
1645
+ GROUP BY
1646
+ SUBSTRING(s.occurred_at FROM 1 FOR 4)
1647
+ ), quality AS (
1648
+ SELECT
1649
+ SUBSTRING(occurred_at FROM 1 FOR 4) AS year,
1650
+ SUM(CAST(amount_cents IS NULL AS INT)) AS unknown_amount_count,
1651
+ SUM(CAST(amount_is_estimated = 1 AND NOT amount_cents IS NULL AS INT)) AS estimated_count,
1652
+ SUM(needs_match_review) AS pending_match_count,
1653
+ SUM(CASE WHEN unassigned_cents > 0 OR invalid_shares = 1 THEN 1 ELSE 0 END) AS unassigned_count
1654
+ FROM _v_spending_quality
1655
+ GROUP BY
1656
+ SUBSTRING(occurred_at FROM 1 FOR 4)
1657
+ )
1658
+ SELECT
1659
+ y.year,
1660
+ COALESCE(a.amount_cents, 0) AS amount_cents,
1661
+ COALESCE(a.amount_cents, 0) / NULLIF(100.0, 0) AS amount_yuan,
1662
+ q.unknown_amount_count,
1663
+ q.estimated_count,
1664
+ q.pending_match_count,
1665
+ q.unassigned_count
1666
+ FROM years AS y
1667
+ LEFT JOIN amounts AS a
1668
+ ON a.year IS NOT DISTINCT FROM y.year
1669
+ JOIN quality AS q
1670
+ ON q.year IS NOT DISTINCT FROM y.year;
1671
+
1672
+ /* q_spend_on_her_2026 */
1673
+ /* q_spend_on_her_2026 */
1674
+ CREATE VIEW q_spend_on_her_2026 AS
1675
+ SELECT
1676
+ COALESCE(SUM(amount_cents), 0) / NULLIF(100.0, 0) AS "value",
1677
+ COALESCE(SUM(amount_cents), 0) AS amount_cents,
1678
+ COALESCE(SUM(unknown_amount_count), 0) AS unknown_amount_count,
1679
+ COALESCE(SUM(estimated_count), 0) AS estimated_count,
1680
+ COALESCE(SUM(pending_match_count), 0) AS pending_match_count,
1681
+ COALESCE(SUM(unassigned_count), 0) AS unassigned_count,
1682
+ (
1683
+ SELECT
1684
+ COUNT(*)
1685
+ FROM v_spending
1686
+ WHERE
1687
+ occurred_at IS NULL
1688
+ ) AS undated_count
1689
+ FROM q_spend_on_her_yearly
1690
+ WHERE
1691
+ year = '2026';
1692
+
1693
+ /* q_spend_on_her_total */
1694
+ /* q_spend_on_her_total */
1695
+ CREATE VIEW q_spend_on_her_total AS
1696
+ SELECT
1697
+ COALESCE(SUM(amount_cents), 0) / NULLIF(100.0, 0) AS "value",
1698
+ COALESCE(SUM(amount_cents), 0) AS amount_cents,
1699
+ COALESCE(SUM(unknown_amount_count), 0) AS unknown_amount_count,
1700
+ COALESCE(SUM(estimated_count), 0) AS estimated_count,
1701
+ COALESCE(SUM(pending_match_count), 0) AS pending_match_count,
1702
+ COALESCE(SUM(unassigned_count), 0) AS unassigned_count
1703
+ FROM q_spend_on_her_yearly;
1704
+
1705
+ /* v_me_spending_monthly */
1706
+ /* v_me_spending_monthly */
1707
+ CREATE VIEW v_me_spending_monthly AS
1708
+ SELECT
1709
+ SUBSTRING(occurred_at FROM 1 FOR 7) AS month,
1710
+ platform,
1711
+ COALESCE(category, '未分类') AS category,
1712
+ COUNT(*) AS row_count,
1713
+ SUM(CASE WHEN needs_match_review = 0 THEN amount_cents END) AS amount_cents,
1714
+ SUM(CASE WHEN needs_match_review = 0 THEN amount_cents END) / NULLIF(100.0, 0) AS amount_yuan,
1715
+ SUM(
1716
+ CASE WHEN needs_match_review = 0 AND amount_is_estimated = 0 THEN amount_cents END
1717
+ ) AS exact_cents,
1718
+ SUM(
1719
+ CASE
1720
+ WHEN needs_match_review = 0 AND amount_is_estimated = 1
1721
+ THEN amount_cents
1722
+ ELSE 0
1723
+ END
1724
+ ) AS estimated_cents,
1725
+ SUM(CASE WHEN needs_match_review = 0 THEN unassigned_cents ELSE 0 END) AS unassigned_cents,
1726
+ SUM(CAST(amount_cents IS NULL AS INT)) AS unknown_amount_count,
1727
+ SUM(needs_match_review) AS pending_match_count,
1728
+ SUM(CASE WHEN needs_match_review = 1 THEN amount_cents ELSE 0 END) AS pending_candidate_cents,
1729
+ SUM(invalid_shares) AS invalid_shares_count
1730
+ FROM _v_spending_quality
1731
+ GROUP BY
1732
+ SUBSTRING(occurred_at FROM 1 FOR 7),
1733
+ platform,
1734
+ COALESCE(category, '未分类');
1735
+
1736
+ /* q_spending_monthly */
1737
+ /* q_spending_monthly */
1738
+ CREATE VIEW q_spending_monthly AS
1739
+ SELECT
1740
+ *
1741
+ FROM v_me_spending_monthly;
1742
+
1743
+ /* v_upcoming_dates */
1744
+ /* v_upcoming_dates */
1745
+ CREATE VIEW v_upcoming_dates AS
1746
+ WITH next_dates AS (
1747
+ SELECT
1748
+ id,
1749
+ MIN(occurs_on) AS next_on
1750
+ FROM _v_date_occurrences
1751
+ WHERE
1752
+ occurs_on >= VAULT_TODAY()
1753
+ AND occurs_on >= COALESCE(first_on, VAULT_CALENDAR_DATE(calendar, date_value, NULL), occurs_on)
1754
+ GROUP BY
1755
+ id
1756
+ ), computed AS (
1757
+ SELECT
1758
+ d.*,
1759
+ n.next_on,
1760
+ CAST(TRUNC(JULIANDAY(n.next_on) - JULIANDAY(VAULT_TODAY())) AS BIGINT) AS days_until,
1761
+ CASE
1762
+ WHEN NOT d.first_on IS NULL AND d.calendar = 'lunar'
1763
+ THEN VAULT_LUNAR_YEAR(n.next_on) - VAULT_LUNAR_YEAR(d.first_on)
1764
+ WHEN NOT d.first_on IS NULL
1765
+ THEN CAST(SUBSTRING(n.next_on FROM 1 FOR 4) AS BIGINT) - CAST(SUBSTRING(d.first_on FROM 1 FOR 4) AS BIGINT)
1766
+ END AS anniversary_no,
1767
+ DATE(n.next_on, PRINTF('-%d days', COALESCE(d.remind_days_before, 0))) AS remind_on
1768
+ FROM important_dates AS d
1769
+ JOIN next_dates AS n
1770
+ ON n.id = d.id
1771
+ )
1772
+ SELECT
1773
+ *
1774
+ FROM computed
1775
+ WHERE
1776
+ days_until <= COALESCE(
1777
+ (
1778
+ SELECT
1779
+ CASE
1780
+ WHEN JSON_TYPE(value_json) = 'integer'
1781
+ THEN GREATEST(0, LEAST(2922, CAST(value_json AS BIGINT)))
1782
+ END
1783
+ FROM settings
1784
+ WHERE
1785
+ key = 'upcoming_days'
1786
+ ),
1787
+ 30
1788
+ );
1789
+
1790
+ /* q_upcoming_dates */
1791
+ /* q_upcoming_dates */
1792
+ CREATE VIEW q_upcoming_dates AS
1793
+ SELECT
1794
+ id,
1795
+ title,
1796
+ person_id,
1797
+ next_on,
1798
+ days_until,
1799
+ anniversary_no,
1800
+ remind_on,
1801
+ sensitivity
1802
+ FROM v_upcoming_dates
1803
+ WHERE
1804
+ person_id IS NULL
1805
+ OR EXISTS(
1806
+ SELECT
1807
+ 1
1808
+ FROM _v_module_people AS p
1809
+ WHERE
1810
+ p.id = person_id
1811
+ );
1812
+
1813
+ /* v_her_wishes_open */
1814
+ /* v_her_wishes_open */
1815
+ CREATE VIEW v_her_wishes_open AS
1816
+ SELECT
1817
+ w.*,
1818
+ COALESCE(a.sensitivity, w.sensitivity) AS effective_sensitivity
1819
+ FROM wishes AS w
1820
+ JOIN _v_module_people AS her
1821
+ ON her.id = w.person_id AND her.module = 'her'
1822
+ LEFT JOIN _v_annotations AS a
1823
+ ON a.target_type = 'wishes' AND a.target_key = w.id
1824
+ WHERE
1825
+ w.deleted_at IS NULL
1826
+ AND w.status = 'confirmed'
1827
+ AND w.wish_state IN ('open', 'planned')
1828
+ AND COALESCE(a.hidden, 0) = 0;
1829
+
1830
+ /* q_wishes_open */
1831
+ /* q_wishes_open */
1832
+ CREATE VIEW q_wishes_open AS
1833
+ SELECT
1834
+ COUNT(*) AS "value"
1835
+ FROM v_her_wishes_open;
1836
+
1837
+ /* v_cycles */
1838
+ /* v_cycles */
1839
+ CREATE VIEW v_cycles AS
1840
+ WITH ordered AS (
1841
+ SELECT
1842
+ c.*,
1843
+ LAG(start_on) OVER (PARTITION BY person_id ORDER BY start_on NULLS FIRST) AS previous_start_on,
1844
+ LEAD(start_on) OVER (PARTITION BY person_id ORDER BY start_on NULLS FIRST) AS next_start_on
1845
+ FROM menstrual_cycles AS c
1846
+ LEFT JOIN _v_annotations AS a
1847
+ ON a.target_type = 'menstrual_cycles' AND a.target_key = c.id
1848
+ WHERE
1849
+ c.deleted_at IS NULL AND c.status = 'confirmed' AND COALESCE(a.hidden, 0) = 0
1850
+ ), intervals AS (
1851
+ SELECT
1852
+ *,
1853
+ CAST(TRUNC(JULIANDAY(next_start_on) - JULIANDAY(start_on)) AS BIGINT) AS cycle_days,
1854
+ CAST(TRUNC(JULIANDAY(start_on) - JULIANDAY(previous_start_on)) AS BIGINT) AS previous_cycle_days,
1855
+ CASE
1856
+ WHEN NOT end_on IS NULL
1857
+ THEN CAST(TRUNC(JULIANDAY(end_on) - JULIANDAY(start_on)) AS BIGINT) + 1
1858
+ END AS period_days
1859
+ FROM ordered
1860
+ ), estimated AS (
1861
+ SELECT
1862
+ *,
1863
+ COUNT(previous_cycle_days) OVER history AS history_intervals,
1864
+ CAST(TRUNC(ROUND(CAST(AVG(previous_cycle_days) OVER history AS DECIMAL))) AS BIGINT) AS estimated_cycle_days
1865
+ FROM intervals
1866
+ WINDOW history AS (
1867
+ PARTITION BY person_id
1868
+ ORDER BY start_on NULLS FIRST
1869
+ ROWS BETWEEN 5 PRECEDING AND CURRENT ROW
1870
+ )
1871
+ )
1872
+ SELECT
1873
+ *,
1874
+ CASE
1875
+ WHEN history_intervals >= 2
1876
+ THEN DATE(start_on, PRINTF('+%d days', estimated_cycle_days))
1877
+ END AS predicted_next_on,
1878
+ '最近最多六个已记录间隔的均值;非医学结论,不能用于避孕' AS prediction_note
1879
+ FROM estimated;
1880
+
1881
+ /* v_her_active_hours */
1882
+ /* v_her_active_hours */
1883
+ CREATE VIEW v_her_active_hours AS
1884
+ SELECT
1885
+ CAST(SUBSTRING(m.sent_at FROM 12 FOR 2) AS BIGINT) AS hour,
1886
+ COUNT(*) AS message_count,
1887
+ COUNT(DISTINCT SUBSTRING(m.sent_at FROM 1 FOR 10)) AS active_days
1888
+ FROM v_messages AS m
1889
+ JOIN _v_module_people AS p
1890
+ ON p.id = m.person_id AND p.module = 'her'
1891
+ WHERE
1892
+ m.hidden = 0 AND NOT m.msg_type IN (80, 81)
1893
+ GROUP BY
1894
+ CAST(SUBSTRING(m.sent_at FROM 12 FOR 2) AS BIGINT);
1895
+
1896
+ /* v_utterances */
1897
+ /* v_utterances */
1898
+ CREATE VIEW v_utterances AS
1899
+ SELECT
1900
+ s.*,
1901
+ r.display_name AS recording_name,
1902
+ STRFTIME(
1903
+ '%Y-%m-%d %H:%M:%f',
1904
+ r.started_at,
1905
+ PRINTF('+%.3f seconds', s.start_ms / NULLIF(1000.0, 0))
1906
+ ) AS spoken_at,
1907
+ CAST(NULL AS TEXT) AS person_id,
1908
+ CAST(NULL AS TEXT) AS person_name,
1909
+ CAST(NULL AS TEXT) AS party,
1910
+ CAST(NULL AS TEXT) AS speaker_confidence,
1911
+ GREATEST(COALESCE(a.hidden, 0), COALESCE(ra.hidden, 0)) AS hidden,
1912
+ CASE
1913
+ WHEN a.sensitivity = 'intimate' OR ra.sensitivity = 'intimate'
1914
+ THEN 'intimate'
1915
+ ELSE COALESCE(a.sensitivity, ra.sensitivity, 'normal')
1916
+ END AS sensitivity
1917
+ FROM audio_sentences AS s
1918
+ JOIN audio_records AS r
1919
+ ON r.sha1 = s.record_sha1
1920
+ LEFT JOIN _v_annotations AS a
1921
+ ON a.target_type = 'sentence'
1922
+ AND a.target_key = s.record_sha1 || ':' || s.sort_index
1923
+ LEFT JOIN _v_annotations AS ra
1924
+ ON ra.target_type = 'recording' AND ra.target_key = s.record_sha1;
1925
+
1926
+ /* v_her_mood_monthly */
1927
+ /* v_her_mood_monthly */
1928
+ CREATE VIEW v_her_mood_monthly AS
1929
+ WITH mood_labels AS (
1930
+ SELECT
1931
+ *,
1932
+ ROW_NUMBER() OVER (
1933
+ PARTITION BY target_type, target_key
1934
+ ORDER BY CASE WHEN basis = 'manual' OR updated_by = 'me' THEN 0 ELSE 1 END NULLS FIRST, updated_at DESC NULLS LAST, id DESC NULLS LAST
1935
+ ) AS priority
1936
+ FROM labels
1937
+ WHERE
1938
+ kind = 'mood' AND deleted_at IS NULL
1939
+ ), observations AS (
1940
+ SELECT
1941
+ 'message' AS source_type,
1942
+ m.msg_id AS source_key,
1943
+ m.sent_at AS occurred_at,
1944
+ l."value" AS mood,
1945
+ m.sensitivity
1946
+ FROM v_messages AS m
1947
+ JOIN _v_module_people AS her
1948
+ ON her.id = m.person_id AND her.module = 'her'
1949
+ JOIN mood_labels AS l
1950
+ ON l.target_type = 'message' AND l.target_key = m.msg_id AND l.priority = 1
1951
+ WHERE
1952
+ m.hidden = 0 AND NOT m.msg_type IN (80, 81)
1953
+ UNION ALL
1954
+ SELECT
1955
+ 'sentence',
1956
+ s.record_sha1 || ':' || s.sort_index,
1957
+ s.spoken_at,
1958
+ l."value",
1959
+ s.sensitivity
1960
+ FROM v_utterances AS s
1961
+ JOIN _v_module_people AS her
1962
+ ON her.id = s.person_id AND her.module = 'her'
1963
+ JOIN mood_labels AS l
1964
+ ON l.target_type = 'sentence'
1965
+ AND l.target_key = s.record_sha1 || ':' || s.sort_index
1966
+ AND l.priority = 1
1967
+ WHERE
1968
+ s.hidden = 0
1969
+ UNION ALL
1970
+ SELECT
1971
+ 'journal',
1972
+ j.id,
1973
+ COALESCE(j.happened_on || ' 00:00:00', j.written_at),
1974
+ COALESCE(l."value", j.mood_polarity),
1975
+ COALESCE(a.sensitivity, j.sensitivity)
1976
+ FROM journal_entries AS j
1977
+ JOIN _v_module_people AS her
1978
+ ON her.id = j.about_person_id AND her.module = 'her'
1979
+ LEFT JOIN mood_labels AS l
1980
+ ON l.target_type = 'journal' AND l.target_key = j.id AND l.priority = 1
1981
+ LEFT JOIN _v_annotations AS a
1982
+ ON a.target_type = 'journal' AND a.target_key = j.id
1983
+ WHERE
1984
+ j.deleted_at IS NULL
1985
+ AND COALESCE(a.hidden, 0) = 0
1986
+ AND NOT COALESCE(l."value", j.mood_polarity) IS NULL
1987
+ )
1988
+ SELECT
1989
+ SUBSTRING(occurred_at FROM 1 FOR 7) AS month,
1990
+ mood,
1991
+ COUNT(*) AS observation_count,
1992
+ SUM(CAST(source_type = 'message' AS INT)) AS message_count,
1993
+ SUM(CAST(source_type = 'sentence' AS INT)) AS sentence_count,
1994
+ SUM(CAST(source_type = 'journal' AS INT)) AS journal_count,
1995
+ CASE
1996
+ WHEN SUM(CAST(sensitivity = 'intimate' AS INT)) > 0
1997
+ THEN 'intimate'
1998
+ WHEN SUM(CAST(sensitivity = 'private' AS INT)) > 0
1999
+ THEN 'private'
2000
+ ELSE 'normal'
2001
+ END AS sensitivity
2002
+ FROM observations
2003
+ GROUP BY
2004
+ SUBSTRING(occurred_at FROM 1 FOR 7),
2005
+ mood;
2006
+
2007
+ /* v_her_preferences */
2008
+ /* v_her_preferences */
2009
+ CREATE VIEW v_her_preferences AS
2010
+ SELECT
2011
+ p.*,
2012
+ COALESCE(a.sensitivity, p.sensitivity) AS effective_sensitivity
2013
+ FROM preferences AS p
2014
+ JOIN _v_module_people AS her
2015
+ ON her.id = p.person_id AND her.module = 'her'
2016
+ LEFT JOIN _v_annotations AS a
2017
+ ON a.target_type = 'preferences' AND a.target_key = p.id
2018
+ WHERE
2019
+ p.deleted_at IS NULL AND p.status = 'confirmed' AND COALESCE(a.hidden, 0) = 0;
2020
+
2021
+ /* v_persons_current */
2022
+ /* v_persons_current */
2023
+ CREATE VIEW v_persons_current AS
2024
+ WITH facts AS (
2025
+ SELECT
2026
+ f.*,
2027
+ ROW_NUMBER() OVER (
2028
+ PARTITION BY person_id, attribute
2029
+ ORDER BY COALESCE(valid_from, '') DESC NULLS LAST, f.updated_at DESC NULLS LAST, f.id DESC NULLS LAST
2030
+ ) AS priority
2031
+ FROM profile_facts AS f
2032
+ LEFT JOIN _v_annotations AS a
2033
+ ON a.target_type = 'profile_facts' AND a.target_key = f.id
2034
+ WHERE
2035
+ f.deleted_at IS NULL
2036
+ AND f.status = 'confirmed'
2037
+ AND COALESCE(a.hidden, 0) = 0
2038
+ AND (
2039
+ f.valid_from IS NULL OR f.valid_from <= VAULT_TODAY()
2040
+ )
2041
+ AND (
2042
+ f.valid_to IS NULL OR f.valid_to >= VAULT_TODAY()
2043
+ )
2044
+ ), profiles AS (
2045
+ SELECT
2046
+ person_id,
2047
+ CAST(JSON_OBJECT_AGG(
2048
+ attribute,
2049
+ JSON_OBJECT(
2050
+ 'value': "value",
2051
+ 'value_num': value_num,
2052
+ 'fact_id': id,
2053
+ 'valid_from': valid_from,
2054
+ 'valid_to': valid_to,
2055
+ 'sensitivity': sensitivity
2056
+ )
2057
+ ) AS TEXT) AS profile_json
2058
+ FROM facts
2059
+ WHERE
2060
+ priority = 1
2061
+ GROUP BY
2062
+ person_id
2063
+ )
2064
+ SELECT
2065
+ p.*,
2066
+ CAST(COALESCE(f.profile_json, '{}') AS TEXT) AS profile_json,
2067
+ CASE
2068
+ WHEN NOT VAULT_CALENDAR_DATE('solar', p.birth_on, NULL) IS NULL
2069
+ AND p.birth_on <= VAULT_TODAY()
2070
+ THEN CAST(SUBSTRING(VAULT_TODAY() FROM 1 FOR 4) AS BIGINT) - CAST(SUBSTRING(p.birth_on FROM 1 FOR 4) AS BIGINT) - CAST((
2071
+ SUBSTRING(VAULT_TODAY() FROM 6) < SUBSTRING(p.birth_on FROM 6)
2072
+ ) AS INT)
2073
+ WHEN p.birth_on IS NULL
2074
+ AND p.birth_year <= CAST(SUBSTRING(VAULT_TODAY() FROM 1 FOR 4) AS BIGINT)
2075
+ THEN CAST(SUBSTRING(VAULT_TODAY() FROM 1 FOR 4) AS BIGINT) - p.birth_year
2076
+ END AS age,
2077
+ CASE WHEN p.birth_on IS NULL AND NOT p.birth_year IS NULL THEN 1 ELSE 0 END AS age_is_approx,
2078
+ CASE
2079
+ WHEN VAULT_CALENDAR_DATE('solar', p.birth_on, NULL) IS NULL
2080
+ THEN NULL
2081
+ WHEN SUBSTRING(p.birth_on FROM 6) < '01-20'
2082
+ THEN '摩羯座'
2083
+ WHEN SUBSTRING(p.birth_on FROM 6) < '02-19'
2084
+ THEN '水瓶座'
2085
+ WHEN SUBSTRING(p.birth_on FROM 6) < '03-21'
2086
+ THEN '双鱼座'
2087
+ WHEN SUBSTRING(p.birth_on FROM 6) < '04-20'
2088
+ THEN '白羊座'
2089
+ WHEN SUBSTRING(p.birth_on FROM 6) < '05-21'
2090
+ THEN '金牛座'
2091
+ WHEN SUBSTRING(p.birth_on FROM 6) < '06-22'
2092
+ THEN '双子座'
2093
+ WHEN SUBSTRING(p.birth_on FROM 6) < '07-23'
2094
+ THEN '巨蟹座'
2095
+ WHEN SUBSTRING(p.birth_on FROM 6) < '08-23'
2096
+ THEN '狮子座'
2097
+ WHEN SUBSTRING(p.birth_on FROM 6) < '09-23'
2098
+ THEN '处女座'
2099
+ WHEN SUBSTRING(p.birth_on FROM 6) < '10-24'
2100
+ THEN '天秤座'
2101
+ WHEN SUBSTRING(p.birth_on FROM 6) < '11-23'
2102
+ THEN '天蝎座'
2103
+ WHEN SUBSTRING(p.birth_on FROM 6) < '12-22'
2104
+ THEN '射手座'
2105
+ ELSE '摩羯座'
2106
+ END AS star_sign,
2107
+ VAULT_ZODIAC(p.birth_on) AS zodiac
2108
+ FROM persons AS p
2109
+ LEFT JOIN profiles AS f
2110
+ ON f.person_id = p.id
2111
+ WHERE
2112
+ p.deleted_at IS NULL;
2113
+
2114
+ /* v_her_profile */
2115
+ /* v_her_profile */
2116
+ CREATE VIEW v_her_profile AS
2117
+ SELECT
2118
+ p.*
2119
+ FROM v_persons_current AS p
2120
+ JOIN _v_module_people AS her
2121
+ ON her.id = p.id AND her.module = 'her'
2122
+ LEFT JOIN _v_annotations AS a
2123
+ ON a.target_type = 'persons' AND a.target_key = p.id
2124
+ WHERE
2125
+ COALESCE(a.hidden, 0) = 0;
2126
+
2127
+ /* v_her_reply_speed */
2128
+ /* v_her_reply_speed */
2129
+ CREATE VIEW v_her_reply_speed AS
2130
+ SELECT
2131
+ *
2132
+ FROM _v_us_reply_daily
2133
+ WHERE
2134
+ sender_module = 'her';
2135
+
2136
+ /* v_intimacy */
2137
+ /* v_intimacy */
2138
+ CREATE VIEW v_intimacy AS
2139
+ WITH numbered AS (
2140
+ SELECT
2141
+ e.*,
2142
+ ROW_NUMBER() OVER (
2143
+ PARTITION BY e.person_id, e.partner_id
2144
+ ORDER BY occurred_at NULLS FIRST, e.id NULLS FIRST
2145
+ ) AS sequence_no,
2146
+ ROW_NUMBER() OVER (
2147
+ PARTITION BY e.person_id, e.partner_id, SUBSTRING(occurred_at FROM 1 FOR 10)
2148
+ ORDER BY occurred_at NULLS FIRST, e.id NULLS FIRST
2149
+ ) AS day_sequence_no,
2150
+ LAG(SUBSTRING(occurred_at FROM 1 FOR 10)) OVER (
2151
+ PARTITION BY e.person_id, e.partner_id
2152
+ ORDER BY occurred_at NULLS FIRST, e.id NULLS FIRST
2153
+ ) AS previous_on
2154
+ FROM intimacy_events AS e
2155
+ LEFT JOIN _v_annotations AS a
2156
+ ON a.target_type = 'intimacy_events' AND a.target_key = e.id
2157
+ WHERE
2158
+ e.deleted_at IS NULL AND e.status = 'confirmed' AND COALESCE(a.hidden, 0) = 0
2159
+ ), cycles AS (
2160
+ SELECT
2161
+ e.*,
2162
+ c.id AS cycle_id,
2163
+ c.start_on AS cycle_start_on,
2164
+ COALESCE(c.next_start_on, c.predicted_next_on) AS reference_next_on,
2165
+ CASE
2166
+ WHEN NOT c.next_start_on IS NULL
2167
+ THEN 'recorded_next_period'
2168
+ WHEN NOT c.predicted_next_on IS NULL
2169
+ THEN 'historical_mean'
2170
+ ELSE 'insufficient_data'
2171
+ END AS window_basis
2172
+ FROM numbered AS e
2173
+ LEFT JOIN v_cycles AS c
2174
+ ON c.person_id = e.person_id
2175
+ AND c.start_on = (
2176
+ SELECT
2177
+ MAX(previous.start_on)
2178
+ FROM v_cycles AS previous
2179
+ WHERE
2180
+ previous.person_id = e.person_id
2181
+ AND previous.start_on <= SUBSTRING(e.occurred_at FROM 1 FOR 10)
2182
+ )
2183
+ )
2184
+ SELECT
2185
+ *,
2186
+ CAST(TRUNC(JULIANDAY(SUBSTRING(occurred_at FROM 1 FOR 10)) - JULIANDAY(previous_on)) AS BIGINT) AS gap_days,
2187
+ CAST(TRUNC(JULIANDAY(SUBSTRING(occurred_at FROM 1 FOR 10)) - JULIANDAY(cycle_start_on)) AS BIGINT) + 1 AS cycle_day,
2188
+ CASE
2189
+ WHEN DATE(reference_next_on, '-19 days') >= cycle_start_on
2190
+ THEN DATE(reference_next_on, '-19 days')
2191
+ END AS estimated_fertile_start_on,
2192
+ CASE
2193
+ WHEN DATE(reference_next_on, '-19 days') >= cycle_start_on
2194
+ THEN DATE(reference_next_on, '-14 days')
2195
+ END AS estimated_fertile_end_on,
2196
+ '日历估算假设排卵在下次来潮前14天,窗口为此前5天至估算日;非医学结论,不能用于避孕' AS window_note
2197
+ FROM cycles;
2198
+
2199
+ /* v_me_active_hours */
2200
+ /* v_me_active_hours */
2201
+ CREATE VIEW v_me_active_hours AS
2202
+ SELECT
2203
+ CAST(SUBSTRING(m.sent_at FROM 12 FOR 2) AS BIGINT) AS hour,
2204
+ COUNT(*) AS message_count,
2205
+ COUNT(DISTINCT SUBSTRING(m.sent_at FROM 1 FOR 10)) AS active_days
2206
+ FROM v_messages AS m
2207
+ JOIN _v_module_people AS p
2208
+ ON p.id = m.person_id AND p.module = 'me'
2209
+ WHERE
2210
+ m.hidden = 0 AND NOT m.msg_type IN (80, 81)
2211
+ GROUP BY
2212
+ CAST(SUBSTRING(m.sent_at FROM 12 FOR 2) AS BIGINT);
2213
+
2214
+ /* v_me_reply_speed */
2215
+ /* v_me_reply_speed */
2216
+ CREATE VIEW v_me_reply_speed AS
2217
+ SELECT
2218
+ *
2219
+ FROM _v_us_reply_daily
2220
+ WHERE
2221
+ sender_module = 'me';
2222
+
2223
+ /* v_timeline */
2224
+ /* v_timeline */
2225
+ CREATE VIEW v_timeline AS
2226
+ WITH visible_events AS (
2227
+ SELECT
2228
+ e.*,
2229
+ COALESCE(a.sensitivity, e.sensitivity) AS effective_sensitivity
2230
+ FROM events AS e
2231
+ LEFT JOIN _v_annotations AS a
2232
+ ON a.target_type = 'events' AND a.target_key = e.id
2233
+ WHERE
2234
+ e.deleted_at IS NULL AND e.status = 'confirmed' AND COALESCE(a.hidden, 0) = 0
2235
+ ), rows AS (
2236
+ SELECT
2237
+ 'events' AS source_type,
2238
+ e.id AS source_key,
2239
+ e.kind,
2240
+ e.start_at AS occurred_at,
2241
+ e.title,
2242
+ e.summary AS detail,
2243
+ e.effective_sensitivity AS sensitivity,
2244
+ e.id AS event_id
2245
+ FROM visible_events AS e
2246
+ UNION ALL
2247
+ SELECT
2248
+ 'important_dates',
2249
+ d.id,
2250
+ d.kind,
2251
+ COALESCE(d.first_on, VAULT_CALENDAR_DATE(d.calendar, d.date_value, NULL)) || ' 00:00:00',
2252
+ d.title,
2253
+ d.note,
2254
+ COALESCE(a.sensitivity, d.sensitivity),
2255
+ NULL
2256
+ FROM important_dates AS d
2257
+ LEFT JOIN _v_annotations AS a
2258
+ ON a.target_type = 'important_dates' AND a.target_key = d.id
2259
+ WHERE
2260
+ d.deleted_at IS NULL AND d.status = 'confirmed' AND COALESCE(a.hidden, 0) = 0
2261
+ UNION ALL
2262
+ SELECT
2263
+ 'gift_ideas',
2264
+ g.id,
2265
+ 'gift',
2266
+ e.start_at,
2267
+ g.title,
2268
+ g.occasion,
2269
+ CASE
2270
+ WHEN COALESCE(a.sensitivity, g.sensitivity) = 'intimate'
2271
+ OR e.effective_sensitivity = 'intimate'
2272
+ THEN 'intimate'
2273
+ WHEN COALESCE(a.sensitivity, g.sensitivity) = 'private'
2274
+ OR e.effective_sensitivity = 'private'
2275
+ THEN 'private'
2276
+ ELSE 'normal'
2277
+ END,
2278
+ e.id
2279
+ FROM gift_ideas AS g
2280
+ JOIN visible_events AS e
2281
+ ON e.id = g.given_event_id
2282
+ LEFT JOIN _v_annotations AS a
2283
+ ON a.target_type = 'gift_ideas' AND a.target_key = g.id
2284
+ WHERE
2285
+ g.deleted_at IS NULL
2286
+ AND g.status = 'confirmed'
2287
+ AND g.idea_state = 'given'
2288
+ AND COALESCE(a.hidden, 0) = 0
2289
+ AND e.kind <> 'gift'
2290
+ UNION ALL
2291
+ SELECT
2292
+ 'wishes',
2293
+ w.id,
2294
+ 'wish_done',
2295
+ e.start_at,
2296
+ w.text,
2297
+ w.category,
2298
+ CASE
2299
+ WHEN COALESCE(a.sensitivity, w.sensitivity) = 'intimate'
2300
+ OR e.effective_sensitivity = 'intimate'
2301
+ THEN 'intimate'
2302
+ WHEN COALESCE(a.sensitivity, w.sensitivity) = 'private'
2303
+ OR e.effective_sensitivity = 'private'
2304
+ THEN 'private'
2305
+ ELSE 'normal'
2306
+ END,
2307
+ e.id
2308
+ FROM wishes AS w
2309
+ JOIN visible_events AS e
2310
+ ON e.id = w.done_event_id
2311
+ LEFT JOIN _v_annotations AS a
2312
+ ON a.target_type = 'wishes' AND a.target_key = w.id
2313
+ WHERE
2314
+ w.deleted_at IS NULL
2315
+ AND w.status = 'confirmed'
2316
+ AND w.wish_state = 'done'
2317
+ AND COALESCE(a.hidden, 0) = 0
2318
+ )
2319
+ SELECT
2320
+ source_type,
2321
+ source_key,
2322
+ kind,
2323
+ occurred_at,
2324
+ title,
2325
+ detail,
2326
+ sensitivity,
2327
+ event_id
2328
+ FROM rows
2329
+ WHERE
2330
+ NOT occurred_at IS NULL;
2331
+
2332
+ /* v_us_spending_unassigned */
2333
+ /* v_us_spending_unassigned */
2334
+ CREATE VIEW v_us_spending_unassigned AS
2335
+ SELECT
2336
+ *,
2337
+ CASE
2338
+ WHEN needs_match_review = 1
2339
+ THEN 'pending_match'
2340
+ WHEN amount_cents IS NULL
2341
+ THEN 'unknown_amount'
2342
+ WHEN invalid_shares = 1
2343
+ THEN 'invalid_shares'
2344
+ ELSE 'unassigned_beneficiary'
2345
+ END AS reason
2346
+ FROM _v_spending_quality
2347
+ WHERE
2348
+ needs_match_review = 1
2349
+ OR amount_cents IS NULL
2350
+ OR invalid_shares = 1
2351
+ OR unassigned_cents > 0;
2352
+
2353
+ /* v_us_timeline */
2354
+ /* v_us_timeline */
2355
+ CREATE VIEW v_us_timeline AS
2356
+ SELECT
2357
+ t.*
2358
+ FROM v_timeline AS t
2359
+ WHERE
2360
+ (
2361
+ t.source_type = 'events'
2362
+ AND EXISTS(
2363
+ SELECT
2364
+ 1
2365
+ FROM _v_us_events AS e
2366
+ WHERE
2367
+ e.id = t.event_id
2368
+ )
2369
+ )
2370
+ OR (
2371
+ t.source_type = 'important_dates'
2372
+ AND EXISTS(
2373
+ SELECT
2374
+ 1
2375
+ FROM important_dates AS d
2376
+ WHERE
2377
+ d.id = t.source_key
2378
+ AND (
2379
+ d.person_id IS NULL
2380
+ OR EXISTS(
2381
+ SELECT
2382
+ 1
2383
+ FROM _v_module_people AS p
2384
+ WHERE
2385
+ p.id = d.person_id
2386
+ )
2387
+ )
2388
+ )
2389
+ )
2390
+ OR (
2391
+ t.source_type IN ('gift_ideas', 'wishes')
2392
+ AND EXISTS(
2393
+ SELECT
2394
+ 1
2395
+ FROM _v_us_events AS e
2396
+ WHERE
2397
+ e.id = t.event_id
2398
+ )
2399
+ AND (
2400
+ (
2401
+ t.source_type = 'gift_ideas'
2402
+ AND EXISTS(
2403
+ SELECT
2404
+ 1
2405
+ FROM gift_ideas AS g
2406
+ JOIN _v_module_people AS p
2407
+ ON p.id = g.for_person_id
2408
+ WHERE
2409
+ g.id = t.source_key
2410
+ )
2411
+ )
2412
+ OR (
2413
+ t.source_type = 'wishes'
2414
+ AND EXISTS(
2415
+ SELECT
2416
+ 1
2417
+ FROM wishes AS w
2418
+ JOIN _v_module_people AS p
2419
+ ON p.id = w.person_id
2420
+ WHERE
2421
+ w.id = t.source_key
2422
+ )
2423
+ )
2424
+ )
2425
+ );