242 Commits
Author SHA1 Message Date
hectorzhao c6f11014d6 test: verify CMPP send guards and channel retry 2026-08-21 12:05:47 +08:00
hectorzhao 03f72c87de docs: record CMPP main flow regression 2026-08-21 11:40:26 +08:00
hectorzhao 2b5256d4a1 docs: record phase four paid stress results 2026-08-21 11:18:19 +08:00
hectorzhao 90345fba22 perf: coalesce batch progress refreshes 2026-08-21 11:07:20 +08:00
hectorzhao 3c6f1beed1 perf: collapse short submit result callbacks 2026-08-21 10:46:27 +08:00
hectorzhao fcf6d3e6b4 perf: remove paid result account lock contention 2026-08-21 10:40:28 +08:00
hectorzhao 487b5282a6 perf: batch paid CMPP accounting and weighted routes 2026-08-21 10:25:40 +08:00
hectorzhao 0176aa6952 docs: record CMPP phase three pressure results 2026-08-21 09:56:31 +08:00
hectorzhao 52028b9bbd perf: batch CMPP inbound workflow processing 2026-08-21 09:44:51 +08:00
hectorzhao 57192b7586 docs: record CMPP phase two pressure results 2026-08-20 19:13:46 +08:00
hectorzhao f4560479d3 fix: type CMPP inbox JSON parameters 2026-08-20 19:04:12 +08:00
hectorzhao 99fb346566 perf: merge CMPP inbox validation and persistence 2026-08-20 18:55:09 +08:00
hectorzhao 633e7a7055 docs(cmpp): record phase one capacity ceiling 2026-08-20 18:39:29 +08:00
hectorzhao 229a0b28fd perf(cmpp): reserve transport for inbound submit 2026-08-20 18:23:01 +08:00
hectorzhao 6708f1f7c5 perf(cmpp): isolate inbound transport capacity 2026-08-20 18:09:29 +08:00
hectorzhao 53073461e9 fix(cmpp): honor UTC inbox retry leases 2026-08-20 17:50:00 +08:00
hectorzhao 0b63bcd74e perf(cmpp): add durable inbound fast path 2026-08-20 17:34:45 +08:00
hectorzhao 26ef67fb6a docs(test): record v5 pressure and priority results 2026-08-20 16:26:44 +08:00
hectorzhao 14f993c1f8 perf(cmpp): reduce inbound database round trips 2026-08-20 15:29:45 +08:00
hectorzhao 67b760a599 perf(cmpp): decouple supplier result callbacks 2026-08-20 14:19:42 +08:00
hectorzhao 485af688d2 docs(cmpp): record V3 test pressure results 2026-08-20 12:34:08 +08:00
hectorzhao e9c73333b3 fix(cmpp): preserve inbound window after message registration 2026-08-20 12:08:49 +08:00
hectorzhao 0757a699ff perf(cmpp): process inbound submits within client window 2026-08-20 11:45:16 +08:00
hectorzhao b9a71fe0b9 perf(cmpp): instrument inbound flow and unbatch submit worker 2026-08-20 11:27:35 +08:00
hectorzhao c4f36fc50d docs: record monitoring read deployment 2026-08-16 15:11:52 +08:00
hectorzhao 482f7ac1ae feat: add monitoring alert read state 2026-08-16 15:04:26 +08:00
hectorzhao 79f5d3f215 docs: record monitoring test deployment 2026-08-14 18:05:19 +08:00
hectorzhao 2216d00d51 fix: preserve Prometheus rule group access 2026-08-14 18:02:42 +08:00
hectorzhao 0cd09441da fix: guard configurable monitoring ratios 2026-08-14 17:59:22 +08:00
hectorzhao 6ccc102830 feat: add configurable infrastructure alerts 2026-08-14 17:51:42 +08:00
hectorzhao 1ef4380422 feat: 完善服务监控与下游重投 2026-08-14 17:21:29 +08:00
hectorzhao d30d9ea4d0 feat: add Fail2ban security detection console 2026-08-14 10:58:18 +08:00
hectorzhao b78faa1aa2 feat: add Prometheus system monitoring 2026-08-14 10:10:28 +08:00
hectorzhao 96e475d60d docs: record downstream requeue release 2026-08-13 12:13:41 +08:00
hectorzhao 433b2ee56f feat: harden downstream requeue tasks 2026-08-13 12:05:57 +08:00
hectorzhao 67fee21616 feat: refine operations UI and transport limits 2026-08-13 10:42:59 +08:00
hectorzhao fb02cbcf39 docs: 记录签名质量热修复发布 2026-08-12 17:56:26 +08:00
hectorzhao 4c70978da4 fix: 修复未报备签名聚合查询 2026-08-12 17:53:30 +08:00
hectorzhao 16135e5a3e style: 更新运营商标签配色 2026-08-12 17:22:43 +08:00
hectorzhao 4994841709 feat: 增强下游重投与签名质量检测 2026-08-12 17:05:53 +08:00
hectorzhao 1d8d6701a6 docs: 记录通道界面优化部署 2026-08-12 13:25:26 +08:00
hectorzhao f350bf5ef3 feat: 优化通道运营商与金额展示 2026-08-12 13:16:25 +08:00
hectorzhao dc358798e9 docs: 记录签名热力图预发布结果 2026-08-12 11:57:25 +08:00
hectorzhao 0cd353450a feat: 优化签名热力图与金额展示 2026-08-12 11:41:47 +08:00
hectorzhao e64b5e23fe docs: record legacy signature migration deployment 2026-08-10 22:59:16 +08:00
hectorzhao 2ecb24cf8d feat: remove legacy signature confirmation 2026-08-10 22:40:13 +08:00
hectorzhao 827d8921a8 docs: record signature retirement deployment 2026-08-10 21:06:09 +08:00
hectorzhao 0eb27e4ac0 fix: wait for services during deployment 2026-08-10 21:00:18 +08:00
hectorzhao 55aa054005 feat: add carrier-aware signature retirement alerts 2026-08-10 20:54:05 +08:00
hectorzhao 232d1c22a3 revert: roll back signature quality period trends 2026-08-09 23:24:50 +08:00
hectorzhao e0c8f82bcf docs: record 35de17a2 preproduction deployment 2026-08-09 23:03:02 +08:00
hectorzhao 35de17a2d4 feat: add signature quality period trends 2026-08-09 22:54:32 +08:00
hectorzhao 608662a054 docs: record 78b839f4 preproduction deployment 2026-08-09 21:04:25 +08:00
hectorzhao 78b839f468 feat: refine template deletion and channel group filters 2026-08-09 20:54:52 +08:00
hectorzhao 482d332f49 docs: record 7804f64c preproduction deployment 2026-08-09 19:17:04 +08:00
hectorzhao 7804f64ced feat: support governed cascade deletion 2026-08-09 19:08:38 +08:00
hectorzhao 6add563ee8 docs: record preproduction deployment for 4724b9db 2026-08-09 15:11:15 +08:00
hectorzhao 4724b9db6a feat: improve operations diagnostics and channel management 2026-08-09 14:27:19 +08:00
hectorzhao 44352aeb2f docs: record RealeseV2.3 preproduction deployment 2026-08-06 11:32:32 +08:00
hectorzhao 8ad8e61793 release: prepare RealeseV2.3 2026-08-06 10:48:36 +08:00
hectorzhao 57b58f1c40 docs: record 530a65de preproduction release 2026-08-03 16:36:04 +08:00
hectorzhao 530a65de80 feat: integrate analytics and fragment receipt improvements 2026-08-03 15:34:41 +08:00
hectorzhao 3357ace7e1 docs: record 94e997ec preproduction release 2026-08-02 20:46:22 +08:00
hectorzhao 94e997ec4d fix: improve retry feedback and deletion stability 2026-08-02 20:39:49 +08:00
hectorzhao ecc3d7a504 docs: record 37ffce40 preproduction release 2026-07-31 23:51:50 +08:00
hectorzhao 37ffce40b2 docs: record successful workspace push 2026-07-31 22:57:39 +08:00
hectorzhao 4b9127e1ab docs: record remote authentication blocker 2026-07-31 22:28:31 +08:00
hectorzhao 461a65f810 docs: record ca4f591a validation and push scope 2026-07-31 22:26:08 +08:00
hectorzhao ca4f591a13 feat: add phone frequency controls and modularize codebase 2026-07-31 22:25:23 +08:00
hectorzhao 0af671b4ed docs: record c0a4317a preproduction release 2026-07-29 22:58:06 +08:00
hectorzhao c0a4317a7e feat: optimize routing and operations views 2026-07-29 22:32:28 +08:00
hectorzhao 500f43f673 fix: restore enterprise application lists 2026-07-29 08:11:51 +08:00
hectorzhao 751c03d421 docs: record list pagination rollout 2026-07-28 23:29:52 +08:00
hectorzhao fbacb74544 fix: compress vite javascript assets 2026-07-28 23:26:07 +08:00
hectorzhao b8560372cc fix: paginate operational list pages 2026-07-28 23:20:13 +08:00
hectorzhao 87c5b1eccc docs: record reporting filter rollout 2026-07-28 20:59:46 +08:00
hectorzhao 95c052e9ea fix: align reporting filters and carrier quality 2026-07-28 20:55:50 +08:00
hectorzhao d8e5319647 docs: record preproduction rollout 2026-07-28 20:34:06 +08:00
hectorzhao 99c8c7c68b feat: complete reporting and filing workflows 2026-07-28 20:28:47 +08:00
hectorzhao 352a6293b4 docs: record signature review deployment 2026-07-27 22:26:27 +08:00
hectorzhao 7c1a0287a0 chore: harden preproduction deployment 2026-07-27 22:17:48 +08:00
hectorzhao df70b336a0 fix: align signature review and admin operations 2026-07-27 22:07:51 +08:00
hectorzhao 9891ffee23 docs: record signature and report deployment 2026-07-27 20:59:45 +08:00
hectorzhao 94aeacd3a2 fix: validate signatures and restore report metrics 2026-07-27 20:51:14 +08:00
hectorzhao 04a497c791 docs: record retry race deployment verification 2026-07-26 23:01:09 +08:00
hectorzhao e0f6eed0d4 fix: make SMS retry side effects idempotent 2026-07-26 22:31:30 +08:00
hectorzhao 04f78695ed docs: record preproduction channel attribution release 2026-07-26 21:43:50 +08:00
hectorzhao 0857de09d8 fix: harden channel retry attribution and operations UI 2026-07-26 21:33:58 +08:00
hectorzhao 059b38e8fe fix: support soft-delete user uniqueness in tools 2026-07-26 13:14:46 +08:00
hectorzhao 2ce682c3fc feat: strengthen risk controls and review workflows 2026-07-26 13:08:12 +08:00
hectorzhao b461532075 fix: mitigate npm dependency advisories 2026-07-26 00:29:49 +08:00
hectorzhao 54617c927e fix: harden upstream and downstream receipt delivery 2026-07-25 23:32:13 +08:00
hectorzhao 5018167696 docs: record daily statistics deployment 2026-07-24 22:16:52 +08:00
hectorzhao 53736c96e8 fix: align daily operations statistics 2026-07-24 22:08:06 +08:00
hectorzhao c78ed1cc32 docs: record daily quality deployment 2026-07-24 18:21:51 +08:00
hectorzhao e12bdf010e fix: restore daily operations quality metrics 2026-07-24 18:13:22 +08:00
hectorzhao 91f04f5288 fix: close receipt delivery workflows 2026-07-24 14:40:57 +08:00
hectorzhao 2ee39056eb docs: record shared receipt deployment 2026-07-24 13:00:54 +08:00
hectorzhao ca12f14b00 fix: reconcile shared-channel receipts and protocol logs 2026-07-24 12:52:06 +08:00
hectorzhao 3997349c21 docs: record protocol log deployment 2026-07-24 11:05:07 +08:00
hectorzhao 0bfeb0839e feat: add protocol interaction observability 2026-07-24 10:47:47 +08:00
hectorzhao 4f4fa5fcb7 docs: clarify TPS contention analysis 2026-07-24 08:27:28 +08:00
hectorzhao 39d2cc5909 docs: record channel recovery deployment 2026-07-24 08:26:39 +08:00
hectorzhao afd3c96070 feat: improve channel resilience and operations 2026-07-24 08:17:33 +08:00
hectorzhao 2f781ebb8a docs: record inbound long message deployment 2026-07-23 21:11:14 +08:00
hectorzhao b29576fcd1 fix: reassemble inbound CMPP long messages 2026-07-23 21:04:35 +08:00
hectorzhao f186aee00b docs: record signature encoding deployment 2026-07-22 18:59:38 +08:00
hectorzhao e28288f691 fix: repair SQL_ASCII signature encoding 2026-07-22 18:51:02 +08:00
hectorzhao 344003e034 docs: record CMPP daily limit deployment 2026-07-22 17:18:37 +08:00
hectorzhao 55019443c0 fix: reject CMPP daily limit synchronously 2026-07-22 17:09:44 +08:00
hectorzhao 66bc230d7e docs: record a09036c6 pre-release deployment 2026-07-22 17:04:47 +08:00
hectorzhao a09036c67b fix: enforce application limits and signature format 2026-07-22 16:29:22 +08:00
hectorzhao cd085d2712 docs: record production deployment 2026-07-22 14:38:32 +08:00
hectorzhao 0f223f7f91 feat: harden platform workflows and UI governance 2026-07-22 14:14:55 +08:00
hectorzhao ef957f7daa docs: record production release verification 2026-07-20 18:16:02 +08:00
hectorzhao f02c33cbb7 feat: harden CMPP delivery and platform workflows 2026-07-20 18:07:29 +08:00
hectorzhao 80fb5a8f53 docs: record application improvements rollout 2026-07-16 18:08:16 +08:00
hectorzhao 23a1f6fa15 fix: recover CMPP slots after gateway restart 2026-07-16 18:02:25 +08:00
hectorzhao faa716b8d0 feat: improve application access and money precision 2026-07-16 17:54:05 +08:00
hectorzhao 9d5c507007 docs: record HTTP API production rollout 2026-07-16 11:42:10 +08:00
hectorzhao dcb6162dcf feat: add HTTP API and complete client workflows 2026-07-16 11:34:06 +08:00
hectorzhao 4f07b331e5 docs: record gateway safeguards production rollout 2026-07-15 18:32:25 +08:00
hectorzhao 85ff037647 fix: restore active channels after gateway restart 2026-07-15 18:30:02 +08:00
hectorzhao 7091a8bed4 feat: add report material workflows and gateway safeguards 2026-07-15 18:23:48 +08:00
hectorzhao cf9f4ce4cd docs: record shared controls production deployment 2026-07-15 16:39:52 +08:00
hectorzhao a7a4e8d9f6 feat: polish reporting templates and shared controls 2026-07-15 16:31:49 +08:00
hectorzhao 28fb8e9038 docs: record admin optimization production rollout 2026-07-15 15:53:09 +08:00
hectorzhao 3e114ee729 feat: add access number routing and admin list improvements 2026-07-15 15:45:27 +08:00
hectorzhao 27c464246b docs: record report production rollout 2026-07-15 14:26:33 +08:00
hectorzhao 8c3336600e feat: add reconciliation and quality reporting 2026-07-15 14:22:50 +08:00
hectorzhao 16311546af docs: record a7586724 production rollout 2026-07-15 12:12:20 +08:00
hectorzhao a758672436 fix: harden dependencies and downstream delivery 2026-07-15 12:05:24 +08:00
hectorzhao 9bbfb72be6 docs: record July 15 production rollout 2026-07-15 11:28:16 +08:00
hectorzhao e47432bc9d fix: harden admin and CMPP delivery workflows 2026-07-15 11:21:02 +08:00
hectorzhao 00b6d95752 docs: record SMS worker production recovery 2026-07-15 09:13:32 +08:00
hectorzhao c1a17699db feat: restore credit limits and harden SMS sending 2026-07-14 18:46:35 +08:00
hectorzhao 3e3b7a9d1a docs: record balance billing deployment 2026-07-14 17:47:53 +08:00
hectorzhao 28dad93e3e fix: simplify balance billing and govern operation logs 2026-07-14 17:40:32 +08:00
hectorzhao f35691f185 docs: record receipt correlation deployment 2026-07-14 16:35:19 +08:00
hectorzhao 1ce02ef206 fix: correlate downstream receipts with submit responses 2026-07-14 16:22:41 +08:00
hectorzhao 135b4fd24e docs: record downstream ack deployment 2026-07-14 14:31:49 +08:00
hectorzhao 8c03663f24 feat: harden sessions and track downstream acknowledgements 2026-07-14 14:18:43 +08:00
hectorzhao 3d37adcc9f docs: record CMPP signature fix deployment 2026-07-14 11:22:14 +08:00
hectorzhao f08a73e191 fix: accept partially reported CMPP signatures 2026-07-14 11:19:24 +08:00
hectorzhao ab29e60ae1 docs: record admin workflow production deployment 2026-07-14 10:10:11 +08:00
hectorzhao 7099147871 fix: preserve legacy report record source accuracy 2026-07-14 10:05:08 +08:00
hectorzhao 6421671259 feat: complete drainage review and admin search workflows 2026-07-14 09:58:18 +08:00
hectorzhao dec2430b7e docs: record legacy signature review deployment 2026-07-13 17:46:23 +08:00
hectorzhao ff3e56070a fix: allow reviewing legacy draft signatures 2026-07-13 17:44:44 +08:00
hectorzhao 6eaa4157e8 docs: record signature workflow deployment 2026-07-13 17:43:05 +08:00
hectorzhao 551b99cbcd feat: complete signature and drainage reporting workflows 2026-07-13 17:40:37 +08:00
hectorzhao 567e4da0c9 docs: record enterprise layout deployment 2026-07-13 10:34:10 +08:00
hectorzhao 19d47b45eb fix: improve enterprise template and signature layouts 2026-07-13 10:31:25 +08:00
hectorzhao d4b2df82da docs: record signature status display deployment 2026-07-13 10:17:40 +08:00
hectorzhao 344b2afe3b fix: clarify signature reporting status 2026-07-13 10:16:12 +08:00
hectorzhao 551edfd8ca docs: record partial report routing deployment 2026-07-13 10:04:40 +08:00
hectorzhao ade06058f5 fix: route through reported signature channels 2026-07-13 10:02:48 +08:00
hectorzhao 7703873346 docs: record signature report status deployment 2026-07-12 20:40:55 +08:00
hectorzhao eab059583f feat: unify signature channel reporting status 2026-07-12 20:38:20 +08:00
hectorzhao 3f3ff8a793 docs: record report requirements deployment 2026-07-12 16:22:04 +08:00
hectorzhao 87ae4a2063 feat: inherit channel report requirements 2026-07-12 16:14:54 +08:00
hectorzhao 053bcca990 docs: record cmpp review aggregation deployment 2026-07-12 13:27:16 +08:00
hectorzhao 8b6ec92f4f feat: aggregate cmpp template mismatch reviews 2026-07-12 13:25:19 +08:00
hectorzhao c8b6d0aa7c docs: record management totals deployment 2026-07-12 10:59:02 +08:00
hectorzhao 390b970032 fix: remove truncated management totals 2026-07-12 10:56:00 +08:00
hectorzhao 24cb632a45 docs: record sms route identity deployment 2026-07-11 21:58:15 +08:00
hectorzhao 7b8424d9bc fix: persist sms carrier and province 2026-07-11 21:55:25 +08:00
hectorzhao 37048a4c9a docs: record downstream connection deployment 2026-07-11 19:19:54 +08:00
hectorzhao 2fea15ef25 fix: track live downstream cmpp connections 2026-07-11 19:16:52 +08:00
hectorzhao 76e0417ccf fix: record cmpp business failures with receipts 2026-07-11 18:30:57 +08:00
hectorzhao 2ecbb920a6 fix: close documented platform polish gaps 2026-07-11 18:20:38 +08:00
hectorzhao 6236547871 docs: record cmpp2 production verification 2026-07-11 11:06:01 +08:00
hectorzhao bb4992f0f9 fix: negotiate downstream cmpp protocol versions 2026-07-11 11:00:42 +08:00
hectorzhao a4d42cf702 fix: standardize currency and input selection styles 2026-07-11 10:49:00 +08:00
hectorzhao 20f89d14aa fix: log cmpp packet decode failures 2026-07-11 10:40:46 +08:00
hectorzhao 208a6c23f8 fix: complete first version issue remediation 2026-07-11 10:14:37 +08:00
hectorzhao 709ac97764 fix: handle existing admin during deploy 2026-07-10 13:48:23 +08:00
hectorzhao 09b78377e6 fix: preserve linux script line endings 2026-07-10 13:45:31 +08:00
hectorzhao d5e668a2e6 feat: scope blacklists and paginate phone segments 2026-07-10 13:42:39 +08:00
hectorzhao ff0756ff9c feat: add application interface controls 2026-07-09 18:32:26 +08:00
hectorzhao 767f0ca9aa fix: correct application cmpp credentials 2026-07-09 17:18:39 +08:00
hectorzhao 57a4c86a79 fix: show sms receipt details with local time 2026-07-09 16:51:24 +08:00
hectorzhao 3ed5f51bae fix: show raw receipt codes in sms details 2026-07-09 16:31:53 +08:00
hectorzhao dc7d015219 fix: handle cmpp2 deliver receipts 2026-07-09 16:14:11 +08:00
hectorzhao 20757203d9 fix: submit cmpp2 packets for cmpp2 channels 2026-07-09 16:01:10 +08:00
hectorzhao 64bb3fb430 fix: show login failure feedback 2026-07-09 15:18:19 +08:00
hectorzhao 268392ee9c fix: preserve production admin password on deploy 2026-07-09 15:08:47 +08:00
hectorzhao 06e6be3c39 fix: align channel copy and gateway connection state 2026-07-09 14:57:55 +08:00
hectorzhao 2a65b41c4e feat: enhance admin sms records view 2026-07-09 12:30:35 +08:00
hectorzhao e0579857c6 fix: decouple channel test sms records 2026-07-09 12:23:43 +08:00
hectorzhao c58a010951 fix: close admin channel test sms loop 2026-07-09 11:49:20 +08:00
hectorzhao a4a638208e fix: correct login entry routing and errors 2026-07-08 18:03:30 +08:00
hectorzhao 8144f08652 feat: complete cmpp gateway delivery recovery workflows 2026-07-08 16:30:06 +08:00
hectorzhao cc628d0214 feat: add cmpp inbound gateway listener 2026-07-07 16:19:14 +08:00
hectorzhao ad3b86ba0a fix: strip prisma schema from pg health check 2026-07-07 15:35:28 +08:00
hectorzhao 3777ef95e8 fix: clean api build output before deploy 2026-07-07 15:34:12 +08:00
hectorzhao a08dd7f58d fix: use detected node path in api service 2026-07-07 15:29:48 +08:00
hectorzhao 7ac364b407 fix: use china go proxy for production build 2026-07-07 15:27:58 +08:00
hectorzhao 3781e07015 fix: install dev dependencies during production build 2026-07-07 15:24:51 +08:00
hectorzhao 07b0c89df6 fix: configure postgres password auth in bootstrap 2026-07-07 15:23:41 +08:00
hectorzhao fb15e1568f fix: use url safe database password in bootstrap 2026-07-07 15:21:33 +08:00
hectorzhao 5a856e0b84 fix: allow local object storage deployment 2026-07-07 15:17:26 +08:00
hectorzhao 994dadb84e fix: skip existing minio in production bootstrap 2026-07-07 15:13:58 +08:00
hectorzhao a35ba4886a fix: add go download fallback for production bootstrap 2026-07-07 15:08:39 +08:00
hectorzhao ac859d5607 fix: install node from official tarball in bootstrap 2026-07-07 15:04:19 +08:00
hectorzhao 9fee6fa81b fix: support arm64 production bootstrap 2026-07-07 15:02:23 +08:00
hectorzhao 4c8f7dd73f feat: add application queue priority routing 2026-07-07 14:49:10 +08:00
hectorzhao 72f2c010ce fix: polish channel groups and add production deployment 2026-07-07 11:16:08 +08:00
hectorzhao b5132d7f4e fix: harden real backend workflows and channel connections 2026-07-06 17:54:53 +08:00
hectorzhao 8cca361441 fix: harden real backend admin workflows and ui 2026-07-03 19:29:56 +08:00
hectorzhao dd09d91c1e fix: enforce carrier-specific channel group routing 2026-07-03 12:48:16 +08:00
hectorzhao a890b03163 fix: implement channel group routing failover 2026-07-03 11:07:43 +08:00
hectorzhao 131f344ac4 fix: connect remaining sms pages to real backend 2026-07-02 19:30:04 +08:00
hectorzhao 06562e42dd docs: update testing execution notes 2026-07-02 16:31:41 +08:00
hectorzhao e26d8324c3 feat: complete login and user management flow 2026-07-02 16:28:48 +08:00
hectorzhao 9c639d54b9 chore: set cmpp port to 17890 2026-07-02 15:59:17 +08:00
hectorzhao 321cf716f2 fix: connect operations pages to real backend 2026-07-02 15:16:34 +08:00
hectorzhao ab421cf8a7 feat: wire admin workflows to real APIs 2026-07-02 13:50:09 +08:00
hectorzhao f8c9b78c21 fix: close sms scheduling and billing gaps 2026-07-01 18:56:05 +08:00
hectorzhao 8ba4ef8a13 test: record step8 final regression 2026-07-01 18:23:08 +08:00
hectorzhao df3df3a771 test: record step7 cmpp status coverage 2026-07-01 18:19:10 +08:00
hectorzhao 118a4179e8 test: record step6 billing coverage 2026-07-01 18:13:16 +08:00
hectorzhao 251989e318 test: record step5 send-chain coverage 2026-07-01 18:08:46 +08:00
hectorzhao 9e03920e61 test: record step4 precheck coverage 2026-07-01 18:01:16 +08:00
hectorzhao 26757cb3b3 docs: record testing step three execution 2026-07-01 17:49:48 +08:00
hectorzhao 7fa91675b8 test: add real environment smoke coverage 2026-07-01 17:42:19 +08:00
hectorzhao bbbbe269a8 docs: record testing step two execution 2026-07-01 17:40:34 +08:00
hectorzhao 1466a28a44 docs: complete testing step one baseline 2026-07-01 17:33:24 +08:00
hectorzhao 7f834b2905 chore: add initial prisma migration 2026-07-01 16:43:08 +08:00
hectorzhao 21d1dd2839 docs: record testing step one execution 2026-07-01 15:57:54 +08:00
hectorzhao 7ff6d7eac3 feat: add manual recharge and operation logs 2026-07-01 15:54:56 +08:00
hectorzhao 50d75f1cf3 docs: add dashboard recharge log and connection test cases 2026-07-01 15:51:16 +08:00
hectorzhao db27ace069 docs: add customer and cmpp status test steps 2026-07-01 15:45:55 +08:00
hectorzhao 19ff6531d1 docs: expand business closure test cases 2026-07-01 15:36:51 +08:00
hectorzhao a31beeac0d docs: add system functional test cases 2026-07-01 15:00:04 +08:00
hectorzhao 91d1e38a09 test: add first-version coverage 2026-07-01 14:46:35 +08:00
hectorzhao 924457a48e feat: add operations acceptance phase 2026-07-01 13:46:54 +08:00
hectorzhao 27bb2d798a feat: add sms send chain phase 2026-07-01 13:39:26 +08:00
hectorzhao 56f835a00f feat: add risk review phase 2026-07-01 13:29:24 +08:00
hectorzhao ee926fea04 feat: complete cmpp platform phases 0-5 2026-07-01 13:22:04 +08:00
hectorzhao 824a8b334f 优化侧栏待开发标签显示 2026-07-01 09:50:30 +08:00
hectorzhao 2675fafb28 标记待开发功能范围 2026-07-01 09:44:10 +08:00
724 changed files with 136613 additions and 13831 deletions
+61
View File
@@ -0,0 +1,61 @@
# Web
VITE_API_BASE_URL=http://localhost:3000/api
# API
API_PORT=3000
DATABASE_URL=postgresql://cmpp:cmpp_password@localhost:5432/cmpp_platform?schema=public
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_URL=redis://127.0.0.1:6379
HTTP_API_MASTER_KEY=replace-with-at-least-32-random-characters
# Customer-facing HTTP API origin returned by the real backend and shown in copied integration parameters.
HTTP_API_PUBLIC_ORIGIN=https://api.example.com
API_ENABLE_SEND_WORKER=true
API_SEND_WORKER_CONCURRENCY=50
API_WORKER_DATABASE_URL=
API_WORKER_METRICS_HOST=127.0.0.1
API_WORKER_METRICS_PORT=9465
CMPP_INBOUND_FAST_PATH_ENABLED=true
CMPP_INBOUND_WORKFLOW_WORKER_ENABLED=true
API_INBOUND_WORKFLOW_CONCURRENCY=32
API_INBOUND_WORKFLOW_BATCH_ENABLED=true
API_INBOUND_WORKFLOW_BATCH_SIZE=64
API_INBOUND_WORKFLOW_POLL_INTERVAL_MS=100
API_INBOUND_WORKFLOW_STALE_SECONDS=300
ADMIN_SESSION_IDLE_TIMEOUT_MS=3600000
CLIENT_SESSION_IDLE_TIMEOUT_MS=7200000
SESSION_LOCK_RECOVERY_MS=14400000
SESSION_ABSOLUTE_TIMEOUT_MS=43200000
SESSION_RECENT_AUTH_MS=1800000
OPERATION_LOG_ARCHIVE_ENABLED=true
OPERATION_LOG_RETENTION_DAYS=180
OPERATION_LOG_ARCHIVE_BATCH_SIZE=1000
OPERATION_LOG_ARCHIVE_MAX_BATCHES=20
OPERATION_LOG_ARCHIVE_INTERVAL_MS=86400000
SMS_RECEIPT_TIMEOUT_SCAN_ENABLED=true
SMS_RECEIPT_TIMEOUT_HOURS=72
SMS_RECEIPT_TIMEOUT_SCAN_INTERVAL_MS=300000
# System monitoring reads only fixed queries from a loopback Prometheus instance.
PROMETHEUS_URL=http://127.0.0.1:9090
PROMETHEUS_QUERY_TIMEOUT_MS=5000
# Local HTTP development only. Production must use HTTPS and true.
SESSION_COOKIE_SECURE=false
MINIO_ENDPOINT=localhost:9000
MINIO_ACCESS_KEY=cmpp_minio
MINIO_SECRET_KEY=cmpp_minio_password
MINIO_BUCKET=cmpp-platform
# Gateway
GATEWAY_HEALTH_ADDR=:8090
GATEWAY_REDIS_ADDR=127.0.0.1:6379
GATEWAY_CMPP_VERSION=3.0
GATEWAY_CMPP_ADDR=127.0.0.1:7890
GATEWAY_CMPP_USER=900001
GATEWAY_CMPP_PASSWORD=888888
GATEWAY_SUBMIT_WORKER_CONCURRENCY=64
GATEWAY_SUBMIT_RESULT_STREAM=gateway.submit.results
GATEWAY_SUBMIT_RESULT_GROUP=cmpp-api-callback
GATEWAY_SUBMIT_RESULT_CONSUMER=gateway-1
GATEWAY_SUBMIT_RESULT_WORKER_CONCURRENCY=8
GATEWAY_CMPP_INBOUND_MAX_CONCURRENCY=64
CMPP_DOWNSTREAM_ACK_TIMEOUT_SECONDS=30
+1
View File
@@ -0,0 +1 @@
*.sh text eol=lf
+6
View File
@@ -2,12 +2,18 @@ node_modules/
dist/ dist/
.env .env
.env.* .env.*
!.env.example
*.local *.local
.local-tools/
.local-data/
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
logs/
gateway/gateway.exe
dump.rdb
.DS_Store .DS_Store
Thumbs.db Thumbs.db
+21
View File
@@ -0,0 +1,21 @@
# CMPP Platform API Spike
阶段 0 的 `api/` 目录用于 NestJS + BullMQ 通信 Spike。当前只定义边界,不展开业务模块。
## 阶段 0 职责
- 生产 `cmpp.submit.commands` 队列消息。
- 消费 `cmpp.submit.results``cmpp.receipt.events``cmpp.uplink.events`
- 使用 `traceId``messageId``channelId` 记录最小链路状态。
- 为 500 条/秒 Spike 提供批量入队脚本或测试入口。
## 不在阶段 0 实现
- 认证、权限、租户完整模型。
- 企业、应用、签名、模板 CRUD。
- 风控、审核、报备、计费真实业务规则。
- PostgreSQL/Prisma 正式 schema。
## 队列契约
`docs/contracts/gateway-queue-messages.schema.json``docs/contracts/examples/` 为准。
+12
View File
@@ -0,0 +1,12 @@
/** @type {import('jest').Config} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.ts$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
roots: ['<rootDir>/src'],
testMatch: ['**/*.spec.ts'],
moduleFileExtensions: ['ts', 'js', 'json'],
clearMocks: true,
};
+9533
View File
File diff suppressed because it is too large Load Diff
+51
View File
@@ -0,0 +1,51 @@
{
"name": "cmpp-platform-api",
"version": "0.1.0",
"private": true,
"type": "commonjs",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"test": "jest --runInBand",
"test:watch": "jest --watch",
"start": "node dist/main.js",
"start:dev": "ts-node src/main.ts",
"prisma:generate": "prisma generate",
"prisma:migrate:dev": "prisma migrate dev",
"prisma:migrate:deploy": "prisma migrate deploy"
},
"dependencies": {
"@nestjs/common": "^11.1.28",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.28",
"@nestjs/platform-express": "^11.1.28",
"@nestjs/swagger": "^11.2.3",
"@prisma/adapter-pg": "^7.9.0",
"@prisma/client": "^7.9.0",
"bullmq": "^5.79.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"brace-expansion": "file:vendor/brace-expansion-compat",
"exceljs": "^4.4.0",
"ioredis": "^5.11.1",
"minio": "^8.0.7",
"pg": "^8.22.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/node": "^25.9.3",
"jest": "^30.4.2",
"prisma": "^7.9.0",
"ts-jest": "^29.4.11",
"ts-node": "^10.9.2",
"typescript": "^6.0.3"
},
"overrides": {
"brace-expansion": "$brace-expansion",
"exceljs": {
"uuid": "11.1.1"
},
"find-my-way": "9.7.0"
}
}
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url:
process.env.DATABASE_URL ??
'postgresql://cmpp:cmpp_password@localhost:5432/cmpp_platform?schema=public',
},
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,3 @@
ALTER TABLE "SmsBatchTask" ADD COLUMN "scheduledAt" TIMESTAMP(3);
ALTER TABLE "SmsBatchTask" ADD COLUMN "canceledAt" TIMESTAMP(3);
CREATE INDEX "SmsBatchTask_status_scheduledAt_idx" ON "SmsBatchTask"("status", "scheduledAt");
@@ -0,0 +1,46 @@
ALTER TABLE "Tenant" ADD COLUMN "certificationStatus" TEXT NOT NULL DEFAULT 'approved';
CREATE TABLE "EnterpriseCertification" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"companyName" TEXT NOT NULL,
"licenseNo" TEXT,
"contactName" TEXT,
"contactPhone" TEXT,
"materials" JSONB,
"status" TEXT NOT NULL DEFAULT 'pending',
"rejectReason" TEXT,
"reviewerId" TEXT,
"submittedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"reviewedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EnterpriseCertification_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "CmppConnectionState" (
"id" TEXT NOT NULL,
"tenantId" TEXT,
"channelId" TEXT NOT NULL,
"connectionId" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'disconnected',
"desiredConnections" INTEGER NOT NULL DEFAULT 1,
"currentConnections" INTEGER NOT NULL DEFAULT 0,
"lastConnectedAt" TIMESTAMP(3),
"lastDisconnectedAt" TIMESTAMP(3),
"lastHeartbeatAt" TIMESTAMP(3),
"reconnectCount" INTEGER NOT NULL DEFAULT 0,
"lastError" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CmppConnectionState_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "EnterpriseCertification_tenantId_status_createdAt_idx" ON "EnterpriseCertification"("tenantId", "status", "createdAt");
CREATE UNIQUE INDEX "CmppConnectionState_channelId_connectionId_key" ON "CmppConnectionState"("channelId", "connectionId");
CREATE INDEX "CmppConnectionState_tenantId_status_idx" ON "CmppConnectionState"("tenantId", "status");
CREATE INDEX "CmppConnectionState_channelId_status_idx" ON "CmppConnectionState"("channelId", "status");
ALTER TABLE "EnterpriseCertification" ADD CONSTRAINT "EnterpriseCertification_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "CmppConnectionState" ADD CONSTRAINT "CmppConnectionState_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "CmppConnectionState" ADD CONSTRAINT "CmppConnectionState_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "SmsChannel"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,10 @@
ALTER TABLE "User"
ADD COLUMN "email" TEXT,
ADD COLUMN "phone" TEXT,
ADD COLUMN "failedLoginCount" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "lockedUntil" TIMESTAMP(3),
ADD COLUMN "lastLoginAt" TIMESTAMP(3),
ADD COLUMN "deletedAt" TIMESTAMP(3);
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
CREATE UNIQUE INDEX "User_phone_key" ON "User"("phone");
@@ -0,0 +1,20 @@
-- Channel-group routing rules for first-version send-chain failover.
ALTER TABLE "SmsChannel" ADD COLUMN "sendRegion" TEXT NOT NULL DEFAULT '全国';
ALTER TABLE "SmsChannelGroup" ADD COLUMN "retryEnabled" BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE "SmsChannelGroup" ADD COLUMN "retryTimeLimitHours" INTEGER NOT NULL DEFAULT 72;
CREATE TABLE "PhoneCarrierRule" (
"id" TEXT NOT NULL,
"carrier" TEXT NOT NULL,
"pattern" TEXT NOT NULL,
"priority" INTEGER NOT NULL DEFAULT 100,
"status" TEXT NOT NULL DEFAULT 'active',
"remark" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PhoneCarrierRule_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "PhoneCarrierRule_status_priority_idx" ON "PhoneCarrierRule"("status", "priority");
@@ -0,0 +1,3 @@
ALTER TABLE "SmsChannelGroup" ADD COLUMN "carrier" TEXT NOT NULL DEFAULT 'mobile';
CREATE UNIQUE INDEX "SmsChannelGroupItem_groupId_province_key" ON "SmsChannelGroupItem"("groupId", "province");
@@ -0,0 +1 @@
ALTER TABLE "SmsApplication" ADD COLUMN "customerUnitPrice" INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,11 @@
-- Make CMPP connection states application-scoped for enterprise application status.
ALTER TABLE "CmppConnectionState" ADD COLUMN "applicationId" TEXT;
ALTER TABLE "CmppConnectionState" ADD CONSTRAINT "CmppConnectionState_applicationId_fkey"
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE SET NULL ON UPDATE CASCADE;
DROP INDEX IF EXISTS "CmppConnectionState_channelId_connectionId_key";
CREATE UNIQUE INDEX "CmppConnectionState_applicationId_channelId_connectionId_key"
ON "CmppConnectionState"("applicationId", "channelId", "connectionId");
CREATE INDEX "CmppConnectionState_applicationId_status_idx"
ON "CmppConnectionState"("applicationId", "status");
@@ -0,0 +1,4 @@
ALTER TABLE "SmsChannelGroup" ADD COLUMN "retryTimeLimitMinutes" INTEGER NOT NULL DEFAULT 720;
UPDATE "SmsChannelGroup"
SET "retryTimeLimitMinutes" = LEAST(GREATEST("retryTimeLimitHours", 1), 72) * 60;
@@ -0,0 +1,2 @@
ALTER TABLE "SmsApplication" ADD COLUMN "queuePriority" TEXT NOT NULL DEFAULT 'normal';
ALTER TABLE "SmsMessageRecord" ADD COLUMN "queuePriority" TEXT NOT NULL DEFAULT 'normal';
@@ -0,0 +1,13 @@
ALTER TABLE "SmsApplication" ADD COLUMN "cmppAccount" TEXT;
WITH numbered AS (
SELECT id, row_number() OVER (ORDER BY "createdAt", id) AS rn
FROM "SmsApplication"
)
UPDATE "SmsApplication" app
SET "cmppAccount" = lpad(((100000 + numbered.rn) % 1000000)::text, 6, '0')
FROM numbered
WHERE app.id = numbered.id;
ALTER TABLE "SmsApplication" ALTER COLUMN "cmppAccount" SET NOT NULL;
CREATE UNIQUE INDEX "SmsApplication_cmppAccount_key" ON "SmsApplication"("cmppAccount");
@@ -0,0 +1,56 @@
-- Add customer-side downstream delivery persistence and uplink matching fields.
ALTER TABLE "SmsUplinkMessage"
ADD COLUMN "applicationId" TEXT,
ADD COLUMN "messageRecordId" TEXT,
ADD COLUMN "matchStatus" TEXT NOT NULL DEFAULT 'unmatched',
ADD COLUMN "matchReason" TEXT;
ALTER TABLE "SmsUplinkMessage"
ADD CONSTRAINT "SmsUplinkMessage_applicationId_fkey"
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "SmsUplinkMessage"
ADD CONSTRAINT "SmsUplinkMessage_messageRecordId_fkey"
FOREIGN KEY ("messageRecordId") REFERENCES "SmsMessageRecord"("id") ON DELETE SET NULL ON UPDATE CASCADE;
CREATE INDEX "SmsUplinkMessage_applicationId_receivedAt_idx" ON "SmsUplinkMessage"("applicationId", "receivedAt");
CREATE INDEX "SmsUplinkMessage_messageRecordId_idx" ON "SmsUplinkMessage"("messageRecordId");
CREATE INDEX "SmsUplinkMessage_matchStatus_idx" ON "SmsUplinkMessage"("matchStatus");
CREATE TABLE "CmppDownstreamDelivery" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"messageRecordId" TEXT,
"messageId" TEXT,
"deliveryType" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"payload" JSONB NOT NULL,
"retryCount" INTEGER NOT NULL DEFAULT 0,
"nextRetryAt" TIMESTAMP(3),
"deliveredAt" TIMESTAMP(3),
"lastError" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CmppDownstreamDelivery_pkey" PRIMARY KEY ("id")
);
ALTER TABLE "CmppDownstreamDelivery"
ADD CONSTRAINT "CmppDownstreamDelivery_tenantId_fkey"
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "CmppDownstreamDelivery"
ADD CONSTRAINT "CmppDownstreamDelivery_applicationId_fkey"
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "CmppDownstreamDelivery"
ADD CONSTRAINT "CmppDownstreamDelivery_messageRecordId_fkey"
FOREIGN KEY ("messageRecordId") REFERENCES "SmsMessageRecord"("id") ON DELETE SET NULL ON UPDATE CASCADE;
CREATE INDEX "CmppDownstreamDelivery_tenantId_status_createdAt_idx" ON "CmppDownstreamDelivery"("tenantId", "status", "createdAt");
CREATE INDEX "CmppDownstreamDelivery_applicationId_status_createdAt_idx" ON "CmppDownstreamDelivery"("applicationId", "status", "createdAt");
CREATE INDEX "CmppDownstreamDelivery_messageId_idx" ON "CmppDownstreamDelivery"("messageId");
CREATE INDEX "CmppDownstreamDelivery_messageRecordId_idx" ON "CmppDownstreamDelivery"("messageRecordId");
CREATE INDEX "CmppDownstreamDelivery_deliveryType_status_idx" ON "CmppDownstreamDelivery"("deliveryType", "status");
@@ -0,0 +1,3 @@
ALTER TABLE "SmsApplication"
ADD COLUMN "cmppMaxConnections" INTEGER NOT NULL DEFAULT 1,
ADD COLUMN "cmppWindowSize" INTEGER NOT NULL DEFAULT 16;
@@ -0,0 +1,38 @@
CREATE TABLE "GatewaySubmitDeadLetter" (
"id" TEXT NOT NULL,
"streamMessageId" TEXT NOT NULL,
"tenantId" TEXT,
"applicationId" TEXT,
"channelId" TEXT,
"traceId" TEXT,
"messageId" TEXT,
"submitId" TEXT,
"status" TEXT NOT NULL DEFAULT 'pending',
"failureCode" TEXT NOT NULL,
"failureMessage" TEXT NOT NULL,
"attempts" INTEGER NOT NULL DEFAULT 1,
"maxAttempts" INTEGER NOT NULL DEFAULT 3,
"commandPayload" JSONB,
"rawPayload" TEXT,
"manualRetryCount" INTEGER NOT NULL DEFAULT 0,
"lastRetryStreamId" TEXT,
"lastRetriedAt" TIMESTAMP(3),
"resolvedAt" TIMESTAMP(3),
"resolvedStatus" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "GatewaySubmitDeadLetter_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "GatewaySubmitDeadLetter_streamMessageId_key" ON "GatewaySubmitDeadLetter"("streamMessageId");
CREATE INDEX "GatewaySubmitDeadLetter_status_createdAt_idx" ON "GatewaySubmitDeadLetter"("status", "createdAt");
CREATE INDEX "GatewaySubmitDeadLetter_tenantId_status_createdAt_idx" ON "GatewaySubmitDeadLetter"("tenantId", "status", "createdAt");
CREATE INDEX "GatewaySubmitDeadLetter_applicationId_status_createdAt_idx" ON "GatewaySubmitDeadLetter"("applicationId", "status", "createdAt");
CREATE INDEX "GatewaySubmitDeadLetter_channelId_status_createdAt_idx" ON "GatewaySubmitDeadLetter"("channelId", "status", "createdAt");
CREATE INDEX "GatewaySubmitDeadLetter_messageId_idx" ON "GatewaySubmitDeadLetter"("messageId");
CREATE INDEX "GatewaySubmitDeadLetter_submitId_idx" ON "GatewaySubmitDeadLetter"("submitId");
ALTER TABLE "GatewaySubmitDeadLetter" ADD CONSTRAINT "GatewaySubmitDeadLetter_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "GatewaySubmitDeadLetter" ADD CONSTRAINT "GatewaySubmitDeadLetter_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "GatewaySubmitDeadLetter" ADD CONSTRAINT "GatewaySubmitDeadLetter_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "SmsChannel"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,33 @@
CREATE TABLE "GatewayDownstreamRecoveryStatus" (
"id" TEXT NOT NULL,
"account" TEXT NOT NULL,
"tenantId" TEXT,
"applicationId" TEXT,
"gatewayInstanceId" TEXT,
"state" TEXT NOT NULL,
"lastAttemptAt" TIMESTAMP(3),
"lastSuccessAt" TIMESTAMP(3),
"lastFailureAt" TIMESTAMP(3),
"nextRetryAt" TIMESTAMP(3),
"attemptCount" INTEGER NOT NULL DEFAULT 0,
"lastError" TEXT,
"lastSkipReason" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "GatewayDownstreamRecoveryStatus_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "GatewayDownstreamRecoveryStatus_account_key" ON "GatewayDownstreamRecoveryStatus"("account");
CREATE INDEX "GatewayDownstreamRecoveryStatus_tenantId_state_updatedAt_idx" ON "GatewayDownstreamRecoveryStatus"("tenantId", "state", "updatedAt");
CREATE INDEX "GatewayDownstreamRecoveryStatus_applicationId_state_updatedAt_idx" ON "GatewayDownstreamRecoveryStatus"("applicationId", "state", "updatedAt");
CREATE INDEX "GatewayDownstreamRecoveryStatus_state_updatedAt_idx" ON "GatewayDownstreamRecoveryStatus"("state", "updatedAt");
CREATE INDEX "GatewayDownstreamRecoveryStatus_nextRetryAt_idx" ON "GatewayDownstreamRecoveryStatus"("nextRetryAt");
ALTER TABLE "GatewayDownstreamRecoveryStatus"
ADD CONSTRAINT "GatewayDownstreamRecoveryStatus_tenantId_fkey"
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "GatewayDownstreamRecoveryStatus"
ADD CONSTRAINT "GatewayDownstreamRecoveryStatus_applicationId_fkey"
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,5 @@
ALTER TABLE "GatewayDownstreamRecoveryStatus"
ADD COLUMN "failureCategory" TEXT;
CREATE INDEX "GatewayDownstreamRecoveryStatus_failureCategory_updatedAt_idx"
ON "GatewayDownstreamRecoveryStatus"("failureCategory", "updatedAt");
@@ -0,0 +1,6 @@
ALTER TABLE "GatewayDownstreamRecoveryStatus"
ADD COLUMN "lockOwner" TEXT,
ADD COLUMN "lockExpiresAt" TIMESTAMP(3);
CREATE INDEX "GatewayDownstreamRecoveryStatus_lockOwner_lockExpiresAt_idx"
ON "GatewayDownstreamRecoveryStatus"("lockOwner", "lockExpiresAt");
@@ -0,0 +1,64 @@
CREATE TABLE "SmsMessageSegmentAudit" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"batchTaskId" TEXT,
"messageRecordId" TEXT NOT NULL,
"submitRecordId" TEXT,
"channelId" TEXT,
"submitId" TEXT NOT NULL,
"attempt" INTEGER NOT NULL DEFAULT 0,
"segmentTotal" INTEGER NOT NULL DEFAULT 1,
"segmentIndex" INTEGER NOT NULL DEFAULT 1,
"sequenceId" INTEGER,
"gatewayMessageId" TEXT,
"submitStatus" TEXT NOT NULL DEFAULT 'queued',
"receiptStatus" TEXT,
"rawStatus" TEXT,
"compensationType" TEXT,
"errorCode" TEXT,
"errorMessage" TEXT,
"submittedAt" TIMESTAMP(3),
"deliveredAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SmsMessageSegmentAudit_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SmsMessageSegmentAudit_messageRecordId_submitId_segmentIndex_key"
ON "SmsMessageSegmentAudit"("messageRecordId", "submitId", "segmentIndex");
CREATE INDEX "SmsMessageSegmentAudit_tenantId_createdAt_idx"
ON "SmsMessageSegmentAudit"("tenantId", "createdAt");
CREATE INDEX "SmsMessageSegmentAudit_messageRecordId_segmentIndex_idx"
ON "SmsMessageSegmentAudit"("messageRecordId", "segmentIndex");
CREATE INDEX "SmsMessageSegmentAudit_submitRecordId_idx"
ON "SmsMessageSegmentAudit"("submitRecordId");
CREATE INDEX "SmsMessageSegmentAudit_gatewayMessageId_idx"
ON "SmsMessageSegmentAudit"("gatewayMessageId");
CREATE INDEX "SmsMessageSegmentAudit_submitStatus_receiptStatus_idx"
ON "SmsMessageSegmentAudit"("submitStatus", "receiptStatus");
ALTER TABLE "SmsMessageSegmentAudit"
ADD CONSTRAINT "SmsMessageSegmentAudit_tenantId_fkey"
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "SmsMessageSegmentAudit"
ADD CONSTRAINT "SmsMessageSegmentAudit_batchTaskId_fkey"
FOREIGN KEY ("batchTaskId") REFERENCES "SmsBatchTask"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "SmsMessageSegmentAudit"
ADD CONSTRAINT "SmsMessageSegmentAudit_messageRecordId_fkey"
FOREIGN KEY ("messageRecordId") REFERENCES "SmsMessageRecord"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "SmsMessageSegmentAudit"
ADD CONSTRAINT "SmsMessageSegmentAudit_submitRecordId_fkey"
FOREIGN KEY ("submitRecordId") REFERENCES "SmsSubmitRecord"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "SmsMessageSegmentAudit"
ADD CONSTRAINT "SmsMessageSegmentAudit_channelId_fkey"
FOREIGN KEY ("channelId") REFERENCES "SmsChannel"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,45 @@
-- CreateTable
CREATE TABLE "SmsUplinkMatchCandidate" (
"id" TEXT NOT NULL,
"uplinkMessageId" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"messageRecordId" TEXT,
"matchSource" TEXT NOT NULL,
"confidence" INTEGER NOT NULL DEFAULT 50,
"reason" TEXT,
"status" TEXT NOT NULL DEFAULT 'pending',
"claimedAt" TIMESTAMP(3),
"claimedById" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SmsUplinkMatchCandidate_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "SmsUplinkMatchCandidate_uplinkMessageId_applicationId_messageRecordId_key" ON "SmsUplinkMatchCandidate"("uplinkMessageId", "applicationId", "messageRecordId");
-- CreateIndex
CREATE INDEX "SmsUplinkMatchCandidate_uplinkMessageId_status_idx" ON "SmsUplinkMatchCandidate"("uplinkMessageId", "status");
-- CreateIndex
CREATE INDEX "SmsUplinkMatchCandidate_tenantId_status_createdAt_idx" ON "SmsUplinkMatchCandidate"("tenantId", "status", "createdAt");
-- CreateIndex
CREATE INDEX "SmsUplinkMatchCandidate_applicationId_status_createdAt_idx" ON "SmsUplinkMatchCandidate"("applicationId", "status", "createdAt");
-- CreateIndex
CREATE INDEX "SmsUplinkMatchCandidate_messageRecordId_idx" ON "SmsUplinkMatchCandidate"("messageRecordId");
-- AddForeignKey
ALTER TABLE "SmsUplinkMatchCandidate" ADD CONSTRAINT "SmsUplinkMatchCandidate_uplinkMessageId_fkey" FOREIGN KEY ("uplinkMessageId") REFERENCES "SmsUplinkMessage"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SmsUplinkMatchCandidate" ADD CONSTRAINT "SmsUplinkMatchCandidate_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SmsUplinkMatchCandidate" ADD CONSTRAINT "SmsUplinkMatchCandidate_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SmsUplinkMatchCandidate" ADD CONSTRAINT "SmsUplinkMatchCandidate_messageRecordId_fkey" FOREIGN KEY ("messageRecordId") REFERENCES "SmsMessageRecord"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1 @@
ALTER TABLE "SmsChannel" ALTER COLUMN "cmppVersion" SET DEFAULT '2.0';
@@ -0,0 +1,9 @@
ALTER TABLE "SmsMessageRecord" ALTER COLUMN "tenantId" DROP NOT NULL;
ALTER TABLE "SmsMessageRecord" ALTER COLUMN "batchTaskId" DROP NOT NULL;
ALTER TABLE "SmsSubmitRecord" ALTER COLUMN "tenantId" DROP NOT NULL;
ALTER TABLE "SmsSubmitRecord" ALTER COLUMN "batchTaskId" DROP NOT NULL;
ALTER TABLE "SmsMessageSegmentAudit" ALTER COLUMN "tenantId" DROP NOT NULL;
ALTER TABLE "SmsReceiptRecord" ALTER COLUMN "tenantId" DROP NOT NULL;
@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "SmsApplication"
ADD COLUMN "interfaceEnabled" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "interfaceType" TEXT NOT NULL DEFAULT 'cmpp20';
@@ -0,0 +1,9 @@
ALTER TABLE "SmsApplication" ADD COLUMN "cmppEnterpriseCode" TEXT;
UPDATE "SmsApplication" AS app
SET "cmppEnterpriseCode" = tenant."code",
"secretHash" = substring(md5(random()::text || clock_timestamp()::text || app."id") from 1 for 16)
FROM "Tenant" AS tenant
WHERE app."tenantId" = tenant."id";
ALTER TABLE "SmsApplication" ALTER COLUMN "cmppEnterpriseCode" SET NOT NULL;
@@ -0,0 +1,20 @@
ALTER TABLE "EnterpriseBlacklist" ADD COLUMN "applicationId" TEXT;
UPDATE "EnterpriseBlacklist" AS blacklist
SET "applicationId" = (
SELECT "id"
FROM "SmsApplication"
WHERE "tenantId" = blacklist."tenantId"
ORDER BY "createdAt" DESC
LIMIT 1
);
DELETE FROM "EnterpriseBlacklist" WHERE "applicationId" IS NULL;
DROP INDEX IF EXISTS "EnterpriseBlacklist_tenantId_phoneNumber_key";
ALTER TABLE "EnterpriseBlacklist" ALTER COLUMN "applicationId" SET NOT NULL;
CREATE UNIQUE INDEX "EnterpriseBlacklist_applicationId_phoneNumber_key" ON "EnterpriseBlacklist"("applicationId", "phoneNumber");
ALTER TABLE "EnterpriseBlacklist" ADD CONSTRAINT "EnterpriseBlacklist_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1 @@
ALTER TABLE "User" ADD COLUMN "sessionVersion" INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,29 @@
CREATE TABLE "CmppDownstreamConnection" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"account" TEXT NOT NULL,
"enterpriseCode" TEXT NOT NULL,
"connectionId" TEXT NOT NULL,
"remoteIp" TEXT,
"protocol" TEXT,
"status" TEXT NOT NULL DEFAULT 'connected',
"connectedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastHeartbeatAt" TIMESTAMP(3),
"lastSubmitAt" TIMESTAMP(3),
"lastDeliverAt" TIMESTAMP(3),
"disconnectedAt" TIMESTAMP(3),
"lastError" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CmppDownstreamConnection_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "CmppDownstreamConnection_connectionId_key" ON "CmppDownstreamConnection"("connectionId");
CREATE INDEX "CmppDownstreamConnection_applicationId_status_updatedAt_idx" ON "CmppDownstreamConnection"("applicationId", "status", "updatedAt");
CREATE INDEX "CmppDownstreamConnection_tenantId_status_updatedAt_idx" ON "CmppDownstreamConnection"("tenantId", "status", "updatedAt");
CREATE INDEX "CmppDownstreamConnection_account_status_idx" ON "CmppDownstreamConnection"("account", "status");
ALTER TABLE "CmppDownstreamConnection" ADD CONSTRAINT "CmppDownstreamConnection_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "CmppDownstreamConnection" ADD CONSTRAINT "CmppDownstreamConnection_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,21 @@
ALTER TABLE "SmsMessageRecord"
ADD COLUMN "carrier" TEXT,
ADD COLUMN "province" TEXT;
UPDATE "SmsMessageRecord" AS message
SET "province" = segment."province"
FROM "PhoneSegment" AS segment
WHERE segment."prefix" = left(message."phoneNumber", 7);
UPDATE "SmsMessageRecord" AS message
SET "carrier" = COALESCE((
SELECT rule."carrier"
FROM "PhoneCarrierRule" AS rule
WHERE rule."status" = 'active'
AND message."phoneNumber" ~ rule."pattern"
ORDER BY rule."priority" ASC, rule."createdAt" ASC
LIMIT 1
), 'mobile');
CREATE INDEX "SmsMessageRecord_carrier_province_idx"
ON "SmsMessageRecord"("carrier", "province");
@@ -0,0 +1,21 @@
ALTER TABLE "SmsSendTask"
ADD COLUMN "sourceType" TEXT NOT NULL DEFAULT 'risk',
ADD COLUMN "aggregationKey" TEXT,
ADD COLUMN "contentHash" TEXT,
ADD COLUMN "windowStartedAt" TIMESTAMP(3),
ADD COLUMN "windowEndsAt" TIMESTAMP(3);
ALTER TABLE "SmsMessageRecord"
ADD COLUMN "signatureId" TEXT,
ADD COLUMN "reviewTaskId" TEXT;
CREATE UNIQUE INDEX "SmsSendTask_aggregationKey_key" ON "SmsSendTask"("aggregationKey");
CREATE INDEX "SmsMessageRecord_reviewTaskId_status_idx" ON "SmsMessageRecord"("reviewTaskId", "status");
ALTER TABLE "SmsMessageRecord"
ADD CONSTRAINT "SmsMessageRecord_signatureId_fkey"
FOREIGN KEY ("signatureId") REFERENCES "SmsSignature"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "SmsMessageRecord"
ADD CONSTRAINT "SmsMessageRecord_reviewTaskId_fkey"
FOREIGN KEY ("reviewTaskId") REFERENCES "SmsSendTask"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,33 @@
ALTER TABLE "ChannelReportField"
ADD COLUMN "drainageFieldId" TEXT,
ADD COLUMN "reportType" TEXT NOT NULL DEFAULT 'both';
CREATE INDEX "ChannelReportField_drainageFieldId_reportType_idx"
ON "ChannelReportField"("drainageFieldId", "reportType");
ALTER TABLE "ChannelReportField"
ADD CONSTRAINT "ChannelReportField_drainageFieldId_fkey"
FOREIGN KEY ("drainageFieldId") REFERENCES "DrainageField"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
CREATE TABLE "DrainageReportMaterial" (
"id" TEXT NOT NULL,
"signatureId" TEXT NOT NULL,
"drainageItemId" TEXT NOT NULL,
"channelId" TEXT NOT NULL,
"fieldCode" TEXT NOT NULL,
"fieldValue" TEXT,
"fileObjectId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DrainageReportMaterial_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "DrainageReportMaterial_signatureId_drainageItemId_channelId_fieldCode_key"
ON "DrainageReportMaterial"("signatureId", "drainageItemId", "channelId", "fieldCode");
CREATE INDEX "DrainageReportMaterial_channelId_fieldCode_idx"
ON "DrainageReportMaterial"("channelId", "fieldCode");
ALTER TABLE "DrainageReportMaterial" ADD CONSTRAINT "DrainageReportMaterial_signatureId_fkey"
FOREIGN KEY ("signatureId") REFERENCES "SmsSignature"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "DrainageReportMaterial" ADD CONSTRAINT "DrainageReportMaterial_channelId_fkey"
FOREIGN KEY ("channelId") REFERENCES "SmsChannel"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,9 @@
UPDATE "DrainageField"
SET "fieldType" = 'string'
WHERE "fieldType" NOT IN ('string', 'image', 'file');
UPDATE "ChannelReportField"
SET "fieldType" = CASE
WHEN "fieldType" IN ('string', 'image', 'file') THEN "fieldType"
ELSE 'string'
END;
@@ -0,0 +1,9 @@
ALTER TABLE "ChannelSignatureReportTask"
ADD COLUMN "reportType" TEXT NOT NULL DEFAULT 'signature',
ADD COLUMN "drainageItemId" TEXT;
CREATE INDEX "ChannelSignatureReportTask_signatureId_drainageItemId_channelId_idx"
ON "ChannelSignatureReportTask"("signatureId", "drainageItemId", "channelId");
CREATE INDEX "ChannelSignatureReportTask_reportType_status_idx"
ON "ChannelSignatureReportTask"("reportType", "status");
@@ -0,0 +1,45 @@
INSERT INTO "ChannelSignatureReportTask" (
"id", "tenantId", "signatureId", "channelId", "reportType", "drainageItemId", "status", "createdAt", "updatedAt"
)
SELECT
'drainage_' || md5(random()::text || clock_timestamp()::text || material."id"),
signature."tenantId",
material."signatureId",
material."channelId",
'drainage',
material."drainageItemId",
'pending',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
FROM (
SELECT DISTINCT ON ("signatureId", "drainageItemId", "channelId")
"id", "signatureId", "drainageItemId", "channelId"
FROM "DrainageReportMaterial"
ORDER BY "signatureId", "drainageItemId", "channelId", "createdAt"
) material
JOIN "SmsSignature" signature ON signature."id" = material."signatureId"
WHERE NOT EXISTS (
SELECT 1
FROM "ChannelSignatureReportTask" task
WHERE task."signatureId" = material."signatureId"
AND task."channelId" = material."channelId"
AND task."reportType" = 'drainage'
AND task."drainageItemId" = material."drainageItemId"
);
INSERT INTO "ChannelSignatureReportRecord" (
"id", "taskId", "channelId", "action", "statusAfter", "reason", "createdAt"
)
SELECT
'drainage_record_' || md5(random()::text || clock_timestamp()::text || task."id"),
task."id",
task."channelId",
'migration_backfill',
task."status",
'从已有引流报备材料回填任务',
CURRENT_TIMESTAMP
FROM "ChannelSignatureReportTask" task
WHERE task."reportType" = 'drainage'
AND NOT EXISTS (
SELECT 1 FROM "ChannelSignatureReportRecord" record WHERE record."taskId" = task."id"
);
@@ -0,0 +1,7 @@
CREATE UNIQUE INDEX "ChannelSignatureReportTask_target_key"
ON "ChannelSignatureReportTask"(
"signatureId",
"channelId",
"reportType",
COALESCE("drainageItemId", '')
);
@@ -0,0 +1,78 @@
CREATE TABLE "SmsDrainageInfo" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"signatureId" TEXT NOT NULL,
"applicationId" TEXT,
"siteName" TEXT NOT NULL,
"url" TEXT NOT NULL,
"remark" TEXT,
"reportValues" JSONB,
"auditStatus" TEXT NOT NULL DEFAULT 'pending',
"rejectReason" TEXT,
"submittedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"reviewedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SmsDrainageInfo_pkey" PRIMARY KEY ("id")
);
CREATE TEMP TABLE drainage_backfill_map AS
SELECT
signature."id" AS "signatureId",
COALESCE(NULLIF(item.value->>'id', ''), item.ordinality::text) AS "oldId",
'drain_' || md5(signature."id" || ':' || COALESCE(NULLIF(item.value->>'id', ''), item.ordinality::text)) AS "newId",
signature."tenantId" AS "tenantId",
signature."applicationId" AS "applicationId",
item.value AS item
FROM "SmsSignature" signature
CROSS JOIN LATERAL jsonb_array_elements(COALESCE(signature."drainageInfo"->'links', '[]'::jsonb)) WITH ORDINALITY AS item(value, ordinality);
INSERT INTO "SmsDrainageInfo" (
"id", "tenantId", "signatureId", "applicationId", "siteName", "url", "remark", "reportValues",
"auditStatus", "submittedAt", "createdAt", "updatedAt"
)
SELECT
"newId",
"tenantId",
"signatureId",
"applicationId",
COALESCE(NULLIF(item->>'siteName', ''), '未命名站点'),
COALESCE(item->>'url', ''),
NULLIF(item->>'remark', ''),
COALESCE(item->'reportValues', '{}'::jsonb),
CASE WHEN item->>'auditStatus' IN ('pending', 'approved', 'rejected') THEN item->>'auditStatus' ELSE 'pending' END,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
FROM drainage_backfill_map;
UPDATE "ChannelSignatureReportTask" task
SET "drainageItemId" = map."newId"
FROM drainage_backfill_map map
WHERE task."signatureId" = map."signatureId"
AND task."reportType" = 'drainage'
AND task."drainageItemId" = map."oldId";
UPDATE "DrainageReportMaterial" material
SET "drainageItemId" = map."newId"
FROM drainage_backfill_map map
WHERE material."signatureId" = map."signatureId"
AND material."drainageItemId" = map."oldId";
DROP TABLE drainage_backfill_map;
CREATE INDEX "SmsDrainageInfo_tenantId_auditStatus_updatedAt_idx"
ON "SmsDrainageInfo"("tenantId", "auditStatus", "updatedAt");
CREATE INDEX "SmsDrainageInfo_signatureId_auditStatus_idx"
ON "SmsDrainageInfo"("signatureId", "auditStatus");
CREATE INDEX "SmsDrainageInfo_applicationId_idx"
ON "SmsDrainageInfo"("applicationId");
ALTER TABLE "SmsDrainageInfo" ADD CONSTRAINT "SmsDrainageInfo_tenantId_fkey"
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "SmsDrainageInfo" ADD CONSTRAINT "SmsDrainageInfo_signatureId_fkey"
FOREIGN KEY ("signatureId") REFERENCES "SmsSignature"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "SmsDrainageInfo" ADD CONSTRAINT "SmsDrainageInfo_applicationId_fkey"
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "ChannelSignatureReportTask" ADD CONSTRAINT "ChannelSignatureReportTask_drainageItemId_fkey"
FOREIGN KEY ("drainageItemId") REFERENCES "SmsDrainageInfo"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
ALTER TABLE "ChannelSignatureReportRecord"
ADD COLUMN "sourceEntry" TEXT NOT NULL DEFAULT 'system';
@@ -0,0 +1,4 @@
UPDATE "ChannelSignatureReportRecord"
SET "sourceEntry" = 'legacy'
WHERE "action" = 'manual_status_change'
AND "sourceEntry" = 'system';
@@ -0,0 +1,32 @@
ALTER TABLE "SmsApplication"
ADD COLUMN "downstreamReceiptRetryEnabled" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "downstreamUplinkRetryEnabled" BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE "CmppDownstreamDelivery"
ADD COLUMN "retryEnabled" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "sentAt" TIMESTAMP(3),
ADD COLUMN "acknowledgedAt" TIMESTAMP(3),
ADD COLUMN "ackDeadlineAt" TIMESTAMP(3),
ADD COLUMN "ackResult" INTEGER,
ADD COLUMN "ackSequenceId" TEXT,
ADD COLUMN "ackMessageId" TEXT,
ADD COLUMN "connectionId" TEXT;
UPDATE "CmppDownstreamDelivery"
SET
"status" = 'unconfirmed',
"sentAt" = "deliveredAt",
"deliveredAt" = NULL,
"lastError" = '历史记录仅确认 Gateway 已写出,未留存 CMPP_DELIVER_RESP'
WHERE "status" = 'delivered';
UPDATE "CmppDownstreamDelivery"
SET "retryEnabled" = CASE
WHEN "deliveryType" = 'uplink' THEN app."downstreamUplinkRetryEnabled"
ELSE app."downstreamReceiptRetryEnabled"
END
FROM "SmsApplication" app
WHERE app.id = "CmppDownstreamDelivery"."applicationId";
CREATE INDEX "CmppDownstreamDelivery_status_ackDeadlineAt_idx"
ON "CmppDownstreamDelivery"("status", "ackDeadlineAt");
@@ -0,0 +1,11 @@
ALTER TABLE "SmsMessageRecord"
ADD COLUMN "cmppSubmitSequenceId" TEXT;
UPDATE "CmppDownstreamDelivery"
SET
"status" = 'unconfirmed',
"deliveredAt" = NULL,
"lastError" = 'CMPP_DELIVER_RESP Msg_Id=0,仅确认协议收包,未确认业务回执关联'
WHERE "deliveryType" = 'receipt'
AND "status" = 'delivered'
AND COALESCE("ackMessageId", '0') = '0';
@@ -0,0 +1,37 @@
CREATE INDEX "OperationLog_createdAt_idx"
ON "OperationLog"("createdAt");
CREATE INDEX "OperationLog_resource_createdAt_idx"
ON "OperationLog"("resource", "createdAt");
CREATE TABLE "OperationLogArchive" (
"originalId" TEXT NOT NULL,
"tenantId" TEXT,
"userId" TEXT,
"action" TEXT NOT NULL,
"resource" TEXT NOT NULL,
"resourceId" TEXT,
"ipAddress" TEXT,
"userAgent" TEXT,
"detail" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL,
"archiveMonth" TEXT NOT NULL,
"archivedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "OperationLogArchive_pkey" PRIMARY KEY ("originalId")
);
CREATE INDEX "OperationLogArchive_tenantId_createdAt_idx"
ON "OperationLogArchive"("tenantId", "createdAt");
CREATE INDEX "OperationLogArchive_userId_createdAt_idx"
ON "OperationLogArchive"("userId", "createdAt");
CREATE INDEX "OperationLogArchive_createdAt_idx"
ON "OperationLogArchive"("createdAt");
CREATE INDEX "OperationLogArchive_resource_createdAt_idx"
ON "OperationLogArchive"("resource", "createdAt");
CREATE INDEX "OperationLogArchive_archiveMonth_idx"
ON "OperationLogArchive"("archiveMonth");
@@ -0,0 +1,14 @@
ALTER TABLE "RechargeOrder" DROP CONSTRAINT IF EXISTS "RechargeOrder_planId_fkey";
DROP TABLE IF EXISTS "BillingPlan";
ALTER TABLE "TenantAccount"
DROP COLUMN "smsUnits",
DROP COLUMN "creditCents";
ALTER TABLE "AccountTransaction"
DROP COLUMN "smsUnits";
ALTER TABLE "RechargeOrder"
DROP COLUMN "planId",
DROP COLUMN "smsUnits";
@@ -0,0 +1,2 @@
ALTER TABLE "TenantAccount"
ADD COLUMN "creditCents" INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,3 @@
ALTER TABLE "CmppDownstreamDelivery"
ADD COLUMN "manualRetryCount" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "lastRetriedAt" TIMESTAMP(3);
@@ -0,0 +1,59 @@
ALTER TABLE "SmsSubmitRecord"
ADD COLUMN "costUnitPrice" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "costAmountCents" INTEGER NOT NULL DEFAULT 0;
UPDATE "SmsSubmitRecord" submit
SET
"costUnitPrice" = channel."unitPrice",
"costAmountCents" = channel."unitPrice" * message."billingUnits"
FROM "SmsChannel" channel, "SmsMessageRecord" message
WHERE submit."channelId" = channel.id
AND submit."messageRecordId" = message.id;
CREATE TABLE "DailyReconciliationReport" (
"id" TEXT NOT NULL,
"reportDate" DATE NOT NULL,
"tenantId" TEXT NOT NULL,
"tenantName" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"applicationName" TEXT NOT NULL,
"sentUnits" INTEGER NOT NULL DEFAULT 0,
"successUnits" INTEGER NOT NULL DEFAULT 0,
"generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DailyReconciliationReport_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "DailyProfitReport" (
"id" TEXT NOT NULL,
"reportDate" DATE NOT NULL,
"dimensionType" TEXT NOT NULL,
"dimensionId" TEXT NOT NULL,
"dimensionName" TEXT NOT NULL,
"tenantId" TEXT,
"tenantName" TEXT,
"applicationId" TEXT,
"channelId" TEXT,
"sentUnits" INTEGER NOT NULL DEFAULT 0,
"successUnits" INTEGER NOT NULL DEFAULT 0,
"revenueCents" INTEGER NOT NULL DEFAULT 0,
"costCents" INTEGER NOT NULL DEFAULT 0,
"profitCents" INTEGER NOT NULL DEFAULT 0,
"profitRateBps" INTEGER NOT NULL DEFAULT 0,
"generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DailyProfitReport_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "DailyReconciliationReport_reportDate_tenantId_applicationId_key"
ON "DailyReconciliationReport"("reportDate", "tenantId", "applicationId");
CREATE INDEX "DailyReconciliationReport_reportDate_idx" ON "DailyReconciliationReport"("reportDate");
CREATE INDEX "DailyReconciliationReport_tenantId_reportDate_idx" ON "DailyReconciliationReport"("tenantId", "reportDate");
CREATE INDEX "DailyReconciliationReport_applicationId_reportDate_idx" ON "DailyReconciliationReport"("applicationId", "reportDate");
CREATE UNIQUE INDEX "DailyProfitReport_reportDate_dimensionType_dimensionId_key"
ON "DailyProfitReport"("reportDate", "dimensionType", "dimensionId");
CREATE INDEX "DailyProfitReport_dimensionType_reportDate_idx" ON "DailyProfitReport"("dimensionType", "reportDate");
CREATE INDEX "DailyProfitReport_tenantId_reportDate_idx" ON "DailyProfitReport"("tenantId", "reportDate");
CREATE INDEX "DailyProfitReport_applicationId_reportDate_idx" ON "DailyProfitReport"("applicationId", "reportDate");
CREATE INDEX "DailyProfitReport_channelId_reportDate_idx" ON "DailyProfitReport"("channelId", "reportDate");
@@ -0,0 +1,4 @@
-- Downstream connection rows represent current live sessions only.
-- Remove historical disconnected/timeout rows left by the previous persistence model.
DELETE FROM "CmppDownstreamConnection"
WHERE "status" <> 'connected';
@@ -0,0 +1,23 @@
CREATE TABLE "CommonReportField" (
"id" TEXT NOT NULL,
"drainageFieldId" TEXT NOT NULL,
"reportType" TEXT NOT NULL,
"required" BOOLEAN NOT NULL DEFAULT false,
"sortOrder" INTEGER NOT NULL DEFAULT 100,
"status" TEXT NOT NULL DEFAULT 'active',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CommonReportField_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "CommonReportField_drainageFieldId_reportType_key"
ON "CommonReportField"("drainageFieldId", "reportType");
CREATE INDEX "CommonReportField_reportType_status_sortOrder_idx"
ON "CommonReportField"("reportType", "status", "sortOrder");
ALTER TABLE "CommonReportField"
ADD CONSTRAINT "CommonReportField_drainageFieldId_fkey"
FOREIGN KEY ("drainageFieldId") REFERENCES "DrainageField"("id")
ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,65 @@
ALTER TABLE "SmsMessageRecord" ADD COLUMN "drainageInfoId" TEXT;
ALTER TABLE "SmsMessageRecord"
ADD CONSTRAINT "SmsMessageRecord_drainageInfoId_fkey"
FOREIGN KEY ("drainageInfoId") REFERENCES "SmsDrainageInfo"("id") ON DELETE SET NULL ON UPDATE CASCADE;
CREATE INDEX "SmsMessageRecord_drainageInfoId_queuedAt_idx"
ON "SmsMessageRecord"("drainageInfoId", "queuedAt");
WITH drainage_matches AS (
SELECT
message.id AS "messageRecordId",
drainage.id AS "drainageInfoId",
ROW_NUMBER() OVER (
PARTITION BY message.id
ORDER BY LENGTH(drainage.url) DESC, drainage."updatedAt" DESC, drainage.id
) AS match_rank,
COUNT(*) OVER (
PARTITION BY message.id, LENGTH(drainage.url)
) AS same_length_matches
FROM "SmsMessageRecord" message
JOIN "SmsDrainageInfo" drainage
ON drainage."signatureId" = message."signatureId"
AND drainage."auditStatus" = 'approved'
AND LENGTH(BTRIM(drainage.url)) > 0
AND POSITION(BTRIM(drainage.url) IN message.content) > 0
WHERE message."drainageInfoId" IS NULL
)
UPDATE "SmsMessageRecord" message
SET "drainageInfoId" = matched."drainageInfoId"
FROM drainage_matches matched
WHERE message.id = matched."messageRecordId"
AND matched.match_rank = 1
AND matched.same_length_matches = 1;
CREATE TABLE "DailyQualityReport" (
"id" TEXT NOT NULL,
"reportDate" DATE NOT NULL,
"dimensionType" TEXT NOT NULL,
"dimensionId" TEXT NOT NULL,
"dimensionName" TEXT NOT NULL,
"tenantId" TEXT,
"tenantName" TEXT,
"applicationId" TEXT,
"channelId" TEXT,
"signatureId" TEXT,
"drainageInfoId" TEXT,
"sentUnits" INTEGER NOT NULL DEFAULT 0,
"successUnits" INTEGER NOT NULL DEFAULT 0,
"successRateBps" INTEGER NOT NULL DEFAULT 0,
"avgArrivalMs" INTEGER,
"generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DailyQualityReport_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "DailyQualityReport_reportDate_dimensionType_dimensionId_key"
ON "DailyQualityReport"("reportDate", "dimensionType", "dimensionId");
CREATE INDEX "DailyQualityReport_dimensionType_reportDate_sentUnits_idx"
ON "DailyQualityReport"("dimensionType", "reportDate", "sentUnits");
CREATE INDEX "DailyQualityReport_tenantId_reportDate_idx" ON "DailyQualityReport"("tenantId", "reportDate");
CREATE INDEX "DailyQualityReport_applicationId_reportDate_idx" ON "DailyQualityReport"("applicationId", "reportDate");
CREATE INDEX "DailyQualityReport_channelId_reportDate_idx" ON "DailyQualityReport"("channelId", "reportDate");
CREATE INDEX "DailyQualityReport_signatureId_reportDate_idx" ON "DailyQualityReport"("signatureId", "reportDate");
CREATE INDEX "DailyQualityReport_drainageInfoId_reportDate_idx" ON "DailyQualityReport"("drainageInfoId", "reportDate");
@@ -0,0 +1,12 @@
ALTER TABLE "SmsApplication"
ADD COLUMN "cmppApplicationExtension" TEXT,
ADD COLUMN "cmppAccessNumberFillEnabled" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "cmppAccessNumberFillPrefix" TEXT,
ADD COLUMN "cmppClientSrcId" TEXT;
CREATE UNIQUE INDEX "SmsApplication_cmppClientSrcId_key"
ON "SmsApplication"("cmppClientSrcId");
ALTER TABLE "SmsMessageRecord"
ADD COLUMN "clientSrcId" TEXT,
ADD COLUMN "applicationExtension" TEXT;
@@ -0,0 +1,134 @@
ALTER TABLE "SmsSignature"
ADD COLUMN "materialVersion" INTEGER NOT NULL DEFAULT 1,
ADD COLUMN "pendingReport" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "reportChangedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE "SmsDrainageInfo"
ADD COLUMN "materialVersion" INTEGER NOT NULL DEFAULT 1,
ADD COLUMN "pendingReport" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "reportChangedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE "ChannelReportField"
ADD COLUMN "exportName" TEXT,
ADD COLUMN "columnWidth" INTEGER NOT NULL DEFAULT 18,
ADD COLUMN "imageWidth" INTEGER NOT NULL DEFAULT 120,
ADD COLUMN "imageHeight" INTEGER NOT NULL DEFAULT 80,
ADD COLUMN "defaultValue" TEXT,
ADD COLUMN "transform" TEXT;
ALTER TABLE "ReportExportFile"
ALTER COLUMN "taskId" DROP NOT NULL,
ADD COLUMN "batchId" TEXT,
ADD COLUMN "channelId" TEXT;
CREATE TABLE "ReportMaterialBatch" (
"id" TEXT NOT NULL,
"batchNo" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'generating',
"createdById" TEXT,
"selectedCount" INTEGER NOT NULL DEFAULT 0,
"channelCount" INTEGER NOT NULL DEFAULT 0,
"fileCount" INTEGER NOT NULL DEFAULT 0,
"errorMessage" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completedAt" TIMESTAMP(3),
CONSTRAINT "ReportMaterialBatch_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "ReportMaterialBatchItem" (
"id" TEXT NOT NULL,
"batchId" TEXT NOT NULL,
"signatureId" TEXT NOT NULL,
"drainageItemId" TEXT,
"reportType" TEXT NOT NULL,
"materialVersion" INTEGER NOT NULL,
"snapshot" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ReportMaterialBatchItem_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "ReportExportFileItem" (
"id" TEXT NOT NULL,
"exportFileId" TEXT NOT NULL,
"batchItemId" TEXT NOT NULL,
"taskId" TEXT NOT NULL,
"rowNumber" INTEGER NOT NULL,
CONSTRAINT "ReportExportFileItem_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "ReportMaterialImportProfile" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"reportType" TEXT NOT NULL,
"tenantId" TEXT,
"applicationId" TEXT,
"sheetName" TEXT,
"headerRowCount" INTEGER NOT NULL DEFAULT 1,
"dataStartRow" INTEGER NOT NULL DEFAULT 2,
"status" TEXT NOT NULL DEFAULT 'active',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ReportMaterialImportProfile_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "ReportMaterialImportProfileColumn" (
"id" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"sourceHeader" TEXT NOT NULL,
"sourceHeaderPath" TEXT,
"sourceColumnIndex" INTEGER NOT NULL,
"targetFieldCode" TEXT NOT NULL,
"targetKind" TEXT NOT NULL DEFAULT 'dynamic',
"fieldType" TEXT NOT NULL DEFAULT 'string',
"required" BOOLEAN NOT NULL DEFAULT false,
"transform" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 100,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ReportMaterialImportProfileColumn_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "ReportMaterialImportBatch" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"applicationId" TEXT,
"profileId" TEXT,
"fileObjectId" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"reportType" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'analyzed',
"sheetName" TEXT NOT NULL,
"headerRowCount" INTEGER NOT NULL DEFAULT 1,
"dataStartRow" INTEGER NOT NULL DEFAULT 2,
"mapping" JSONB NOT NULL,
"preview" JSONB,
"result" JSONB,
"rowCount" INTEGER NOT NULL DEFAULT 0,
"successCount" INTEGER NOT NULL DEFAULT 0,
"failedCount" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completedAt" TIMESTAMP(3),
CONSTRAINT "ReportMaterialImportBatch_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "ReportMaterialBatch_batchNo_key" ON "ReportMaterialBatch"("batchNo");
CREATE INDEX "ReportMaterialBatch_status_createdAt_idx" ON "ReportMaterialBatch"("status", "createdAt");
CREATE INDEX "ReportMaterialBatchItem_batchId_reportType_idx" ON "ReportMaterialBatchItem"("batchId", "reportType");
CREATE INDEX "ReportMaterialBatchItem_signatureId_drainageItemId_idx" ON "ReportMaterialBatchItem"("signatureId", "drainageItemId");
CREATE UNIQUE INDEX "ReportExportFileItem_exportFileId_batchItemId_key" ON "ReportExportFileItem"("exportFileId", "batchItemId");
CREATE INDEX "ReportExportFileItem_taskId_idx" ON "ReportExportFileItem"("taskId");
CREATE INDEX "ReportExportFile_batchId_channelId_idx" ON "ReportExportFile"("batchId", "channelId");
CREATE INDEX "ReportMaterialImportProfile_reportType_status_idx" ON "ReportMaterialImportProfile"("reportType", "status");
CREATE INDEX "ReportMaterialImportProfile_tenantId_applicationId_idx" ON "ReportMaterialImportProfile"("tenantId", "applicationId");
CREATE UNIQUE INDEX "ReportMaterialImportProfileColumn_profileId_sourceColumnIndex_key" ON "ReportMaterialImportProfileColumn"("profileId", "sourceColumnIndex");
CREATE INDEX "ReportMaterialImportProfileColumn_profileId_sortOrder_idx" ON "ReportMaterialImportProfileColumn"("profileId", "sortOrder");
CREATE INDEX "ReportMaterialImportBatch_tenantId_createdAt_idx" ON "ReportMaterialImportBatch"("tenantId", "createdAt");
CREATE INDEX "ReportMaterialImportBatch_status_createdAt_idx" ON "ReportMaterialImportBatch"("status", "createdAt");
ALTER TABLE "ReportExportFile" ADD CONSTRAINT "ReportExportFile_batchId_fkey" FOREIGN KEY ("batchId") REFERENCES "ReportMaterialBatch"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ReportMaterialBatchItem" ADD CONSTRAINT "ReportMaterialBatchItem_batchId_fkey" FOREIGN KEY ("batchId") REFERENCES "ReportMaterialBatch"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ReportMaterialBatchItem" ADD CONSTRAINT "ReportMaterialBatchItem_signatureId_fkey" FOREIGN KEY ("signatureId") REFERENCES "SmsSignature"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "ReportMaterialBatchItem" ADD CONSTRAINT "ReportMaterialBatchItem_drainageItemId_fkey" FOREIGN KEY ("drainageItemId") REFERENCES "SmsDrainageInfo"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "ReportExportFileItem" ADD CONSTRAINT "ReportExportFileItem_exportFileId_fkey" FOREIGN KEY ("exportFileId") REFERENCES "ReportExportFile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ReportExportFileItem" ADD CONSTRAINT "ReportExportFileItem_batchItemId_fkey" FOREIGN KEY ("batchItemId") REFERENCES "ReportMaterialBatchItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ReportExportFileItem" ADD CONSTRAINT "ReportExportFileItem_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "ChannelSignatureReportTask"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ReportMaterialImportProfileColumn" ADD CONSTRAINT "ReportMaterialImportProfileColumn_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "ReportMaterialImportProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,18 @@
DROP INDEX IF EXISTS "ChannelReportField_channelId_code_key";
INSERT INTO "ChannelReportField" (
"id", "channelId", "drainageFieldId", "reportType", "code", "name", "exportName", "fieldType",
"required", "description", "sortOrder", "columnWidth", "imageWidth", "imageHeight", "defaultValue",
"transform", "status", "createdAt", "updatedAt"
)
SELECT
"id" || '_drainage', "channelId", "drainageFieldId", 'drainage', "code", "name", "exportName", "fieldType",
"required", "description", "sortOrder", "columnWidth", "imageWidth", "imageHeight", "defaultValue",
"transform", "status", "createdAt", "updatedAt"
FROM "ChannelReportField"
WHERE "reportType" = 'both';
UPDATE "ChannelReportField" SET "reportType" = 'signature' WHERE "reportType" = 'both';
CREATE UNIQUE INDEX "ChannelReportField_channelId_code_reportType_key"
ON "ChannelReportField"("channelId", "code", "reportType");
@@ -0,0 +1,4 @@
-- Existing rows predate the pending-material workflow and must not be treated as newly changed.
-- New rows continue to use the schema default (true), while later edits explicitly set true.
UPDATE "SmsSignature" SET "pendingReport" = false;
UPDATE "SmsDrainageInfo" SET "pendingReport" = false;
@@ -0,0 +1 @@
ALTER TABLE "SmsChannelGroupItem" DROP COLUMN "rateLimitPerSecond";
@@ -0,0 +1,165 @@
CREATE TABLE "SmsApplicationHttpConfig" (
"id" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT false,
"sendEnabled" BOOLEAN NOT NULL DEFAULT false,
"messageQueryEnabled" BOOLEAN NOT NULL DEFAULT false,
"receiptWebhookEnabled" BOOLEAN NOT NULL DEFAULT false,
"uplinkWebhookEnabled" BOOLEAN NOT NULL DEFAULT false,
"uplinkQueryEnabled" BOOLEAN NOT NULL DEFAULT false,
"credentialSelfServiceEnabled" BOOLEAN NOT NULL DEFAULT false,
"qpsLimit" INTEGER NOT NULL DEFAULT 10,
"timestampToleranceSeconds" INTEGER NOT NULL DEFAULT 300,
"maxCredentialCount" INTEGER NOT NULL DEFAULT 2,
"uplinkRetentionDays" INTEGER NOT NULL DEFAULT 90,
"maxQueryRangeDays" INTEGER NOT NULL DEFAULT 31,
"maxPageSize" INTEGER NOT NULL DEFAULT 100,
"receiptDeliveryMode" TEXT NOT NULL DEFAULT 'cmpp',
"uplinkDeliveryMode" TEXT NOT NULL DEFAULT 'cmpp',
"webhookRetryEnabled" BOOLEAN NOT NULL DEFAULT true,
"webhookMaxAttempts" INTEGER NOT NULL DEFAULT 7,
"webhookTimeoutSeconds" INTEGER NOT NULL DEFAULT 10,
"requireHttps" BOOLEAN NOT NULL DEFAULT true,
"allowClientManualRetry" BOOLEAN NOT NULL DEFAULT true,
"allowClientTest" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SmsApplicationHttpConfig_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "SmsApplicationHttpIpAllowlist" (
"id" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"ipCidr" TEXT NOT NULL,
"remark" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SmsApplicationHttpIpAllowlist_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "HttpApiCredential" (
"id" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"accessKey" TEXT NOT NULL,
"secretEncrypted" TEXT NOT NULL,
"secretLast4" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'active',
"expiresAt" TIMESTAMP(3),
"lastUsedAt" TIMESTAMP(3),
"lastUsedIp" TEXT,
"createdById" TEXT,
"revokedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HttpApiCredential_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "OpenApiRequest" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"credentialId" TEXT NOT NULL,
"requestId" TEXT NOT NULL,
"idempotencyKey" TEXT NOT NULL,
"bodyHash" TEXT NOT NULL,
"clientMessageId" TEXT,
"messageRecordId" TEXT,
"httpStatus" INTEGER,
"businessCode" TEXT,
"responseBody" JSONB,
"sourceIp" TEXT,
"userAgent" TEXT,
"durationMs" INTEGER,
"status" TEXT NOT NULL DEFAULT 'processing',
"completedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "OpenApiRequest_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "HttpWebhookEndpoint" (
"id" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"eventType" TEXT NOT NULL,
"url" TEXT NOT NULL,
"secretEncrypted" TEXT NOT NULL,
"secretLast4" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'active',
"lastTestAt" TIMESTAMP(3),
"lastTestStatus" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HttpWebhookEndpoint_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "HttpWebhookEvent" (
"id" TEXT NOT NULL,
"eventId" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"eventType" TEXT NOT NULL,
"messageRecordId" TEXT,
"messageId" TEXT,
"uplinkMessageId" TEXT,
"payload" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "HttpWebhookEvent_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "HttpWebhookDelivery" (
"id" TEXT NOT NULL,
"eventId" TEXT NOT NULL,
"endpointId" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"attemptCount" INTEGER NOT NULL DEFAULT 0,
"nextRetryAt" TIMESTAMP(3),
"lastHttpStatus" INTEGER,
"lastError" TEXT,
"deliveredAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HttpWebhookDelivery_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "HttpWebhookAttempt" (
"id" TEXT NOT NULL,
"deliveryId" TEXT NOT NULL,
"attemptNo" INTEGER NOT NULL,
"requestHeaders" JSONB,
"responseStatus" INTEGER,
"responseSummary" TEXT,
"errorMessage" TEXT,
"durationMs" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "HttpWebhookAttempt_pkey" PRIMARY KEY ("id")
);
ALTER TABLE "SmsMessageRecord" ADD COLUMN "clientMessageId" TEXT;
CREATE UNIQUE INDEX "SmsApplicationHttpConfig_applicationId_key" ON "SmsApplicationHttpConfig"("applicationId");
CREATE UNIQUE INDEX "SmsApplicationHttpIpAllowlist_applicationId_ipCidr_key" ON "SmsApplicationHttpIpAllowlist"("applicationId", "ipCidr");
CREATE UNIQUE INDEX "HttpApiCredential_accessKey_key" ON "HttpApiCredential"("accessKey");
CREATE INDEX "HttpApiCredential_applicationId_status_idx" ON "HttpApiCredential"("applicationId", "status");
CREATE UNIQUE INDEX "OpenApiRequest_requestId_key" ON "OpenApiRequest"("requestId");
CREATE UNIQUE INDEX "OpenApiRequest_applicationId_idempotencyKey_key" ON "OpenApiRequest"("applicationId", "idempotencyKey");
CREATE INDEX "OpenApiRequest_applicationId_createdAt_idx" ON "OpenApiRequest"("applicationId", "createdAt");
CREATE UNIQUE INDEX "HttpWebhookEndpoint_applicationId_eventType_key" ON "HttpWebhookEndpoint"("applicationId", "eventType");
CREATE UNIQUE INDEX "HttpWebhookEvent_eventId_key" ON "HttpWebhookEvent"("eventId");
CREATE INDEX "HttpWebhookEvent_applicationId_createdAt_idx" ON "HttpWebhookEvent"("applicationId", "createdAt");
CREATE UNIQUE INDEX "HttpWebhookDelivery_eventId_endpointId_key" ON "HttpWebhookDelivery"("eventId", "endpointId");
CREATE INDEX "HttpWebhookDelivery_status_nextRetryAt_idx" ON "HttpWebhookDelivery"("status", "nextRetryAt");
CREATE UNIQUE INDEX "HttpWebhookAttempt_deliveryId_attemptNo_key" ON "HttpWebhookAttempt"("deliveryId", "attemptNo");
CREATE UNIQUE INDEX "SmsMessageRecord_applicationId_clientMessageId_key" ON "SmsMessageRecord"("applicationId", "clientMessageId");
ALTER TABLE "SmsApplicationHttpConfig" ADD CONSTRAINT "SmsApplicationHttpConfig_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "SmsApplicationHttpIpAllowlist" ADD CONSTRAINT "SmsApplicationHttpIpAllowlist_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "HttpApiCredential" ADD CONSTRAINT "HttpApiCredential_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "OpenApiRequest" ADD CONSTRAINT "OpenApiRequest_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "OpenApiRequest" ADD CONSTRAINT "OpenApiRequest_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "OpenApiRequest" ADD CONSTRAINT "OpenApiRequest_credentialId_fkey" FOREIGN KEY ("credentialId") REFERENCES "HttpApiCredential"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "HttpWebhookEndpoint" ADD CONSTRAINT "HttpWebhookEndpoint_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "HttpWebhookEvent" ADD CONSTRAINT "HttpWebhookEvent_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "HttpWebhookEvent" ADD CONSTRAINT "HttpWebhookEvent_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "HttpWebhookDelivery" ADD CONSTRAINT "HttpWebhookDelivery_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "HttpWebhookEvent"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "HttpWebhookDelivery" ADD CONSTRAINT "HttpWebhookDelivery_endpointId_fkey" FOREIGN KEY ("endpointId") REFERENCES "HttpWebhookEndpoint"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "HttpWebhookAttempt" ADD CONSTRAINT "HttpWebhookAttempt_deliveryId_fkey" FOREIGN KEY ("deliveryId") REFERENCES "HttpWebhookDelivery"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,41 @@
-- Monetary values were historically stored as integer cents. The platform now
-- stores integer units of 0.0001 yuan. Convert existing values by 100 while
-- widening columns to bigint so balances and report aggregates do not inherit
-- PostgreSQL integer's ~214,748 yuan ceiling at the new scale.
ALTER TABLE "TenantAccount"
ALTER COLUMN "balanceCents" TYPE BIGINT USING ("balanceCents"::BIGINT * 100),
ALTER COLUMN "creditCents" TYPE BIGINT USING ("creditCents"::BIGINT * 100);
ALTER TABLE "AccountTransaction"
ALTER COLUMN "amountCents" TYPE BIGINT USING ("amountCents"::BIGINT * 100),
ALTER COLUMN "balanceAfter" TYPE BIGINT USING ("balanceAfter"::BIGINT * 100);
ALTER TABLE "BillingRule"
ALTER COLUMN "unitPrice" TYPE BIGINT USING ("unitPrice"::BIGINT * 100);
ALTER TABLE "RechargeOrder"
ALTER COLUMN "amountCents" TYPE BIGINT USING ("amountCents"::BIGINT * 100);
ALTER TABLE "SmsBillingRecord"
ALTER COLUMN "unitPrice" TYPE BIGINT USING ("unitPrice"::BIGINT * 100),
ALTER COLUMN "amountCents" TYPE BIGINT USING ("amountCents"::BIGINT * 100);
ALTER TABLE "SmsApplication"
ALTER COLUMN "customerUnitPrice" TYPE BIGINT USING ("customerUnitPrice"::BIGINT * 100);
ALTER TABLE "SmsChannel"
ALTER COLUMN "unitPrice" TYPE BIGINT USING ("unitPrice"::BIGINT * 100);
ALTER TABLE "SmsMessageRecord"
ALTER COLUMN "unitPrice" TYPE BIGINT USING ("unitPrice"::BIGINT * 100),
ALTER COLUMN "amountCents" TYPE BIGINT USING ("amountCents"::BIGINT * 100);
ALTER TABLE "SmsSubmitRecord"
ALTER COLUMN "costUnitPrice" TYPE BIGINT USING ("costUnitPrice"::BIGINT * 100),
ALTER COLUMN "costAmountCents" TYPE BIGINT USING ("costAmountCents"::BIGINT * 100);
ALTER TABLE "DailyProfitReport"
ALTER COLUMN "revenueCents" TYPE BIGINT USING ("revenueCents"::BIGINT * 100),
ALTER COLUMN "costCents" TYPE BIGINT USING ("costCents"::BIGINT * 100),
ALTER COLUMN "profitCents" TYPE BIGINT USING ("profitCents"::BIGINT * 100);
@@ -0,0 +1,2 @@
ALTER TABLE "SmsMessageRecord"
ADD COLUMN "cmppSubmitGroupMessageId" TEXT;
@@ -0,0 +1,26 @@
ALTER TABLE "SmsMessageRecord"
ADD COLUMN "receiptRawStatus" TEXT;
ALTER TABLE "SmsReceiptRecord"
ADD COLUMN "receiptKey" TEXT,
ADD COLUMN "errorMessage" TEXT;
UPDATE "SmsReceiptRecord"
SET "receiptKey" = MD5(
COALESCE("channelId", '') || E'\u0001' ||
"gatewayMessageId" || E'\u0001' ||
"receiptStatus" || E'\u0001' ||
"rawStatus"
) || MD5("id");
ALTER TABLE "SmsReceiptRecord"
ALTER COLUMN "receiptKey" SET NOT NULL;
CREATE UNIQUE INDEX "SmsReceiptRecord_receiptKey_key"
ON "SmsReceiptRecord"("receiptKey");
CREATE INDEX "SmsSubmitRecord_channelId_gatewayMessageId_idx"
ON "SmsSubmitRecord"("channelId", "gatewayMessageId");
CREATE INDEX "SmsReceiptRecord_channelId_gatewayMessageId_idx"
ON "SmsReceiptRecord"("channelId", "gatewayMessageId");
@@ -0,0 +1,9 @@
ALTER TABLE "DailyReconciliationReport"
ADD COLUMN "failedUnits" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "DailyProfitReport"
ADD COLUMN "failedUnits" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "refundCents" BIGINT NOT NULL DEFAULT 0;
ALTER TABLE "DailyQualityReport"
ADD COLUMN "failedUnits" INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,11 @@
ALTER TABLE "SmsReceiptRecord"
ADD COLUMN "phoneNumber" TEXT;
UPDATE "SmsReceiptRecord" receipt
SET "phoneNumber" = message."phoneNumber"
FROM "SmsMessageRecord" message
WHERE receipt."messageRecordId" = message.id
AND receipt."phoneNumber" IS NULL;
CREATE INDEX "SmsReceiptRecord_channelId_gatewayMessageId_phoneNumber_idx"
ON "SmsReceiptRecord"("channelId", "gatewayMessageId", "phoneNumber");
@@ -0,0 +1,69 @@
-- Historical receipts created before channel-aware matching could be attached to
-- the right message but retain the wrong channel when two channels shared the
-- same supplier account. Only repair rows that have exactly one submit record
-- for the same message, upstream message id and destination phone number.
WITH unique_receipt_submit AS (
SELECT
receipt.id AS receipt_id,
MIN(submit."channelId") AS correct_channel_id
FROM "SmsReceiptRecord" receipt
JOIN "SmsMessageRecord" message
ON message.id = receipt."messageRecordId"
JOIN "SmsSubmitRecord" submit
ON submit."messageRecordId" = message.id
AND submit."gatewayMessageId" = receipt."gatewayMessageId"
WHERE receipt."receiptStatus" = 'delivered'
AND receipt."phoneNumber" = message."phoneNumber"
GROUP BY receipt.id
HAVING COUNT(submit.id) = 1
)
UPDATE "SmsReceiptRecord" receipt
SET "channelId" = matched.correct_channel_id
FROM unique_receipt_submit matched
WHERE receipt.id = matched.receipt_id
AND receipt."channelId" IS DISTINCT FROM matched.correct_channel_id;
-- Aggregate the corrected successful receipt onto the message master record.
-- DISTINCT ON chooses the first successful receipt for segmented messages while
-- preserving the original submit/gateway identity of the current attempt.
WITH delivered_match AS (
SELECT DISTINCT ON (message.id)
message.id AS message_record_id,
submit."channelId" AS channel_id,
receipt."gatewayMessageId" AS gateway_message_id,
receipt."rawStatus" AS raw_status,
receipt."errorCode" AS error_code,
receipt."errorMessage" AS error_message,
receipt."deliveredAt" AS delivered_at
FROM "SmsMessageRecord" message
JOIN "SmsReceiptRecord" receipt
ON receipt."messageRecordId" = message.id
AND receipt."receiptStatus" = 'delivered'
AND receipt."phoneNumber" = message."phoneNumber"
JOIN "SmsSubmitRecord" submit
ON submit."messageRecordId" = message.id
AND submit."gatewayMessageId" = receipt."gatewayMessageId"
AND submit."channelId" = receipt."channelId"
WHERE message."gatewayMessageId" = receipt."gatewayMessageId"
ORDER BY message.id, receipt."deliveredAt" ASC, receipt.id ASC
)
UPDATE "SmsMessageRecord" message
SET
status = 'delivered',
"receiptStatus" = 'delivered',
"receiptRawStatus" = matched.raw_status,
"channelId" = matched.channel_id,
"gatewayMessageId" = matched.gateway_message_id,
"errorCode" = matched.error_code,
"errorMessage" = matched.error_message,
"deliveredAt" = matched.delivered_at,
"timeoutAt" = NULL,
"updatedAt" = CURRENT_TIMESTAMP
FROM delivered_match matched
WHERE message.id = matched.message_record_id
AND (
message.status IS DISTINCT FROM 'delivered'
OR message."receiptStatus" IS DISTINCT FROM 'delivered'
OR message."deliveredAt" IS DISTINCT FROM matched.delivered_at
OR message."channelId" IS DISTINCT FROM matched.channel_id
);
@@ -0,0 +1,75 @@
-- Enforce a real per-application daily send quota and make 100,000 the platform default.
UPDATE "SmsApplication"
SET "dailyLimit" = 100000
WHERE "dailyLimit" IS NULL;
ALTER TABLE "SmsApplication"
ALTER COLUMN "dailyLimit" SET DEFAULT 100000,
ALTER COLUMN "dailyLimit" SET NOT NULL;
CREATE TABLE "SmsApplicationDailyUsage" (
"id" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"usageDate" DATE NOT NULL,
"usedCount" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SmsApplicationDailyUsage_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SmsApplicationDailyUsage_applicationId_usageDate_key"
ON "SmsApplicationDailyUsage"("applicationId", "usageDate");
CREATE INDEX "SmsApplicationDailyUsage_usageDate_idx"
ON "SmsApplicationDailyUsage"("usageDate");
ALTER TABLE "SmsApplicationDailyUsage"
ADD CONSTRAINT "SmsApplicationDailyUsage_applicationId_fkey"
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
-- Preserve sends already accepted earlier on the deployment day, so deploying at
-- midday cannot grant an extra full quota. The business day is Asia/Shanghai.
INSERT INTO "SmsApplicationDailyUsage" (
"id", "applicationId", "usageDate", "usedCount", "createdAt", "updatedAt"
)
SELECT
md5(message."applicationId" || ':' || ((CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')::date)::text),
message."applicationId",
(CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')::date,
COUNT(*)::integer,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
FROM "SmsMessageRecord" message
WHERE message."applicationId" IS NOT NULL
AND message."queuedAt" >= ((CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')::date::timestamp AT TIME ZONE 'Asia/Shanghai')
AND message."queuedAt" < (((CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')::date + 1)::timestamp AT TIME ZONE 'Asia/Shanghai')
GROUP BY message."applicationId";
-- HTTP applications created before the UI default fix retained CMPP delivery modes.
-- Existing enabled webhook capabilities are migrated to the HTTP transport they expose.
UPDATE "SmsApplicationHttpConfig"
SET "receiptDeliveryMode" = 'http'
WHERE "enabled" = TRUE
AND "receiptWebhookEnabled" = TRUE
AND "receiptDeliveryMode" = 'cmpp';
UPDATE "SmsApplicationHttpConfig"
SET "uplinkDeliveryMode" = 'http'
WHERE "enabled" = TRUE
AND "uplinkWebhookEnabled" = TRUE
AND "uplinkDeliveryMode" = 'cmpp';
ALTER TABLE "SmsApplicationHttpConfig"
ALTER COLUMN "sendEnabled" SET DEFAULT TRUE,
ALTER COLUMN "messageQueryEnabled" SET DEFAULT TRUE,
ALTER COLUMN "receiptWebhookEnabled" SET DEFAULT TRUE,
ALTER COLUMN "uplinkWebhookEnabled" SET DEFAULT TRUE,
ALTER COLUMN "uplinkQueryEnabled" SET DEFAULT TRUE,
ALTER COLUMN "credentialSelfServiceEnabled" SET DEFAULT TRUE,
ALTER COLUMN "receiptDeliveryMode" SET DEFAULT 'http',
ALTER COLUMN "uplinkDeliveryMode" SET DEFAULT 'http';
-- Rollback guidance:
-- Drop SmsApplicationDailyUsage and its FK/indexes, restore SmsApplication.dailyLimit
-- to nullable/no-default, and restore HTTP column defaults to FALSE/'cmpp'. Data migrated
-- from cmpp to http is intentionally not auto-reversed because explicit edits after upgrade
-- cannot be distinguished safely.
@@ -0,0 +1,20 @@
-- SMS signature names are canonical business identifiers and must contain
-- exactly one complete pair of Chinese black brackets: 【signature】.
WITH normalized AS (
SELECT
id,
btrim(
regexp_replace(
regexp_replace(btrim(name), '^[【\[]+', ''),
'[】\]]+$',
''
)
) AS inner_name
FROM "SmsSignature"
)
UPDATE "SmsSignature" AS signature
SET name = '' || normalized.inner_name || ''
FROM normalized
WHERE signature.id = normalized.id
AND normalized.inner_name <> ''
AND signature.name IS DISTINCT FROM '' || normalized.inner_name || '';
@@ -0,0 +1,19 @@
-- The previous bracket-normalization migration ran against a legacy SQL_ASCII
-- database. PostgreSQL treated the UTF-8 bracket regexp as individual bytes and
-- removed the trailing 0x91 byte from the final Chinese character in two known
-- signatures. Restore only the rows whose primary key and corrupt byte sequence
-- both match the pre-migration backup, so later user edits cannot be overwritten.
UPDATE "SmsSignature"
SET name = convert_from(
decode('e38090e888aae5a4a9e4bfa1e681afe4bfa1e8afbae7bd91e38091', 'hex'),
'SQL_ASCII'
)
WHERE id IN (
'cmrj2ci1r00544pnkt64wza4e',
'cmrobgqp7002nwenke5wh09fr'
)
AND encode(convert_to(name, 'SQL_ASCII'), 'hex') =
'e38090e888aae5a4a9e4bfa1e681afe4bfa1e8afbae7bde38091';
-- Rollback guidance: do not restore the invalid byte sequence. If rollback is
-- required, keep the repaired UTF-8 values and only roll back application code.
@@ -0,0 +1,61 @@
CREATE TABLE "CmppInboundLongMessage" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"groupKey" TEXT NOT NULL,
"account" TEXT NOT NULL,
"srcId" TEXT,
"phoneNumbers" JSONB NOT NULL,
"concatReference" INTEGER NOT NULL,
"segmentTotal" INTEGER NOT NULL,
"msgFmt" INTEGER NOT NULL,
"messageId" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'collecting',
"response" JSONB,
"expiresAt" TIMESTAMP(3) NOT NULL,
"completedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CmppInboundLongMessage_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "CmppInboundLongMessageSegment" (
"id" TEXT NOT NULL,
"groupId" TEXT NOT NULL,
"segmentIndex" INTEGER NOT NULL,
"sequenceId" TEXT,
"content" TEXT NOT NULL,
"contentHash" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CmppInboundLongMessageSegment_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "CmppInboundLongMessage_messageId_key"
ON "CmppInboundLongMessage"("messageId");
CREATE INDEX "CmppInboundLongMessage_groupKey_status_createdAt_idx"
ON "CmppInboundLongMessage"("groupKey", "status", "createdAt");
CREATE INDEX "CmppInboundLongMessage_applicationId_status_expiresAt_idx"
ON "CmppInboundLongMessage"("applicationId", "status", "expiresAt");
CREATE UNIQUE INDEX "CmppInboundLongMessageSegment_groupId_segmentIndex_key"
ON "CmppInboundLongMessageSegment"("groupId", "segmentIndex");
CREATE INDEX "CmppInboundLongMessageSegment_sequenceId_idx"
ON "CmppInboundLongMessageSegment"("sequenceId");
ALTER TABLE "CmppInboundLongMessage"
ADD CONSTRAINT "CmppInboundLongMessage_tenantId_fkey"
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "CmppInboundLongMessage"
ADD CONSTRAINT "CmppInboundLongMessage_applicationId_fkey"
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "CmppInboundLongMessageSegment"
ADD CONSTRAINT "CmppInboundLongMessageSegment_groupId_fkey"
FOREIGN KEY ("groupId") REFERENCES "CmppInboundLongMessage"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,14 @@
ALTER TABLE "CmppConnectionState"
ADD COLUMN "lastReconnectAttemptAt" TIMESTAMP(3),
ADD COLUMN "nextReconnectAt" TIMESTAMP(3),
ADD COLUMN "lastErrorCategory" TEXT;
CREATE INDEX "CmppConnectionState_status_nextReconnectAt_idx"
ON "CmppConnectionState"("status", "nextReconnectAt");
-- Rollback:
-- DROP INDEX "CmppConnectionState_status_nextReconnectAt_idx";
-- ALTER TABLE "CmppConnectionState"
-- DROP COLUMN "lastErrorCategory",
-- DROP COLUMN "nextReconnectAt",
-- DROP COLUMN "lastReconnectAttemptAt";
@@ -0,0 +1,2 @@
ALTER TABLE "SmsApplication"
ALTER COLUMN "maxPhonesPerTask" SET DEFAULT 10000;
@@ -0,0 +1,23 @@
ALTER TABLE "DailyReconciliationReport"
ADD COLUMN "submittedUnits" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "unknownUnits" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "DailyProfitReport"
ADD COLUMN "submittedUnits" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "unknownUnits" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "DailyQualityReport"
ADD COLUMN "submittedUnits" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "unknownUnits" INTEGER NOT NULL DEFAULT 0;
UPDATE "DailyReconciliationReport"
SET "submittedUnits" = "sentUnits",
"unknownUnits" = GREATEST("sentUnits" - "successUnits" - "failedUnits", 0);
UPDATE "DailyProfitReport"
SET "submittedUnits" = "sentUnits",
"unknownUnits" = GREATEST("sentUnits" - "successUnits" - "failedUnits", 0);
UPDATE "DailyQualityReport"
SET "submittedUnits" = "sentUnits",
"unknownUnits" = GREATEST("sentUnits" - "successUnits" - "failedUnits", 0);
@@ -0,0 +1,21 @@
WITH ranked AS (
SELECT
id,
ROW_NUMBER() OVER (
PARTITION BY "channelId", "connectionId"
ORDER BY "updatedAt" DESC, "createdAt" DESC, id DESC
) AS row_no
FROM "CmppConnectionState"
WHERE "applicationId" IS NULL
)
DELETE FROM "CmppConnectionState" state
USING ranked
WHERE state.id = ranked.id
AND ranked.row_no > 1;
CREATE UNIQUE INDEX "CmppConnectionState_supplier_channel_connection_key"
ON "CmppConnectionState"("channelId", "connectionId")
WHERE "applicationId" IS NULL;
-- Rollback:
-- DROP INDEX "CmppConnectionState_supplier_channel_connection_key";
@@ -0,0 +1,34 @@
CREATE TABLE "ProtocolInteractionLog" (
"id" TEXT NOT NULL,
"protocol" TEXT NOT NULL,
"direction" TEXT NOT NULL,
"eventType" TEXT NOT NULL,
"status" TEXT NOT NULL,
"tenantId" TEXT,
"applicationId" TEXT,
"channelId" TEXT,
"account" TEXT,
"messageId" TEXT,
"gatewayMessageId" TEXT,
"traceId" TEXT,
"requestId" TEXT,
"phoneMasked" TEXT,
"resultCode" TEXT,
"durationMs" INTEGER,
"payloadBytes" INTEGER,
"retryCount" INTEGER,
"detail" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ProtocolInteractionLog_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "ProtocolInteractionLog_createdAt_idx" ON "ProtocolInteractionLog"("createdAt");
CREATE INDEX "ProtocolInteractionLog_protocol_direction_createdAt_idx" ON "ProtocolInteractionLog"("protocol", "direction", "createdAt");
CREATE INDEX "ProtocolInteractionLog_eventType_createdAt_idx" ON "ProtocolInteractionLog"("eventType", "createdAt");
CREATE INDEX "ProtocolInteractionLog_status_createdAt_idx" ON "ProtocolInteractionLog"("status", "createdAt");
CREATE INDEX "ProtocolInteractionLog_channelId_createdAt_idx" ON "ProtocolInteractionLog"("channelId", "createdAt");
CREATE INDEX "ProtocolInteractionLog_applicationId_createdAt_idx" ON "ProtocolInteractionLog"("applicationId", "createdAt");
CREATE INDEX "ProtocolInteractionLog_messageId_idx" ON "ProtocolInteractionLog"("messageId");
CREATE INDEX "ProtocolInteractionLog_gatewayMessageId_idx" ON "ProtocolInteractionLog"("gatewayMessageId");
CREATE INDEX "ProtocolInteractionLog_requestId_idx" ON "ProtocolInteractionLog"("requestId");
@@ -0,0 +1,17 @@
UPDATE "SmsApplicationHttpConfig" config
SET
"receiptDeliveryMode" = CASE
WHEN application."interfaceEnabled" AND config.enabled THEN 'both'
WHEN application."interfaceEnabled" THEN 'cmpp'
WHEN config.enabled THEN 'http'
ELSE 'none'
END,
"uplinkDeliveryMode" = CASE
WHEN application."interfaceEnabled" AND config.enabled THEN 'both'
WHEN application."interfaceEnabled" THEN 'cmpp'
WHEN config.enabled THEN 'http'
ELSE 'none'
END,
"updatedAt" = CURRENT_TIMESTAMP
FROM "SmsApplication" application
WHERE application.id = config."applicationId";
@@ -0,0 +1,86 @@
CREATE TABLE "CmppDownstreamDeliveryAttempt" (
"id" TEXT NOT NULL,
"deliveryId" TEXT NOT NULL,
"attemptKey" TEXT NOT NULL,
"attemptNo" INTEGER NOT NULL,
"connectionId" TEXT,
"sequenceId" TEXT,
"messageId" TEXT,
"status" TEXT NOT NULL,
"sentAt" TIMESTAMP(3),
"ackDeadlineAt" TIMESTAMP(3),
"acknowledgedAt" TIMESTAMP(3),
"ackResult" INTEGER,
"failureType" TEXT,
"errorMessage" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CmppDownstreamDeliveryAttempt_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "UpstreamReceiptInbox" (
"id" TEXT NOT NULL,
"receiptKey" TEXT NOT NULL,
"incomingChannelId" TEXT NOT NULL,
"incomingConnectionId" TEXT,
"upstreamAccount" TEXT NOT NULL,
"upstreamHost" TEXT NOT NULL,
"upstreamPort" INTEGER NOT NULL,
"protocol" TEXT NOT NULL,
"protocolVersion" TEXT NOT NULL,
"provisionalMessageId" TEXT,
"sequenceId" INTEGER,
"gatewayMessageId" TEXT NOT NULL,
"phoneNumber" TEXT,
"receiptStatus" TEXT NOT NULL,
"rawStatus" TEXT NOT NULL,
"errorCode" TEXT,
"errorMessage" TEXT,
"deliveredAt" TIMESTAMP(3) NOT NULL,
"receivedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"status" TEXT NOT NULL DEFAULT 'pending',
"matchedMessageRecordId" TEXT,
"matchedSubmitRecordId" TEXT,
"matchedChannelId" TEXT,
"attemptCount" INTEGER NOT NULL DEFAULT 0,
"nextRetryAt" TIMESTAMP(3),
"lastError" TEXT,
"processedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UpstreamReceiptInbox_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "CmppDownstreamDeliveryAttempt_attemptKey_key"
ON "CmppDownstreamDeliveryAttempt"("attemptKey");
CREATE INDEX "CmppDownstreamDeliveryAttempt_deliveryId_attemptNo_idx"
ON "CmppDownstreamDeliveryAttempt"("deliveryId", "attemptNo");
CREATE INDEX "CmppDownstreamDeliveryAttempt_status_ackDeadlineAt_idx"
ON "CmppDownstreamDeliveryAttempt"("status", "ackDeadlineAt");
CREATE INDEX "CmppDownstreamDeliveryAttempt_connectionId_sequenceId_idx"
ON "CmppDownstreamDeliveryAttempt"("connectionId", "sequenceId");
CREATE UNIQUE INDEX "UpstreamReceiptInbox_receiptKey_key"
ON "UpstreamReceiptInbox"("receiptKey");
CREATE INDEX "UpstreamReceiptInbox_status_nextRetryAt_receivedAt_idx"
ON "UpstreamReceiptInbox"("status", "nextRetryAt", "receivedAt");
CREATE INDEX "UpstreamReceiptInbox_gatewayMessageId_phoneNumber_idx"
ON "UpstreamReceiptInbox"("gatewayMessageId", "phoneNumber");
CREATE INDEX "UpstreamReceiptInbox_incomingChannelId_receivedAt_idx"
ON "UpstreamReceiptInbox"("incomingChannelId", "receivedAt");
CREATE INDEX "UpstreamReceiptInbox_matchedMessageRecordId_idx"
ON "UpstreamReceiptInbox"("matchedMessageRecordId");
ALTER TABLE "CmppDownstreamDeliveryAttempt"
ADD CONSTRAINT "CmppDownstreamDeliveryAttempt_deliveryId_fkey"
FOREIGN KEY ("deliveryId") REFERENCES "CmppDownstreamDelivery"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,15 @@
DROP INDEX IF EXISTS "User_username_key";
DROP INDEX IF EXISTS "User_email_key";
DROP INDEX IF EXISTS "User_phone_key";
CREATE UNIQUE INDEX "User_active_username_key"
ON "User" ("username")
WHERE "deletedAt" IS NULL;
CREATE UNIQUE INDEX "User_active_email_key"
ON "User" ("email")
WHERE "deletedAt" IS NULL AND "email" IS NOT NULL;
CREATE UNIQUE INDEX "User_active_phone_key"
ON "User" ("phone")
WHERE "deletedAt" IS NULL AND "phone" IS NOT NULL;
@@ -0,0 +1,7 @@
ALTER TABLE "SmsApplication"
ADD COLUMN "disablingAt" TIMESTAMP(3),
ADD COLUMN "autoDisableAt" TIMESTAMP(3),
ADD COLUMN "disableReason" TEXT;
CREATE INDEX "SmsApplication_status_autoDisableAt_idx"
ON "SmsApplication"("status", "autoDisableAt");
@@ -0,0 +1,39 @@
-- Risk thresholds now inherit from global rules and may be overridden per application.
-- Existing maxPhonesPerTask values are intentionally discarded because this environment
-- contains test-only application data and the product owner explicitly declined migration.
ALTER TABLE "RiskRule" ADD COLUMN "applicationId" TEXT;
-- Retain historical rules and hit records for audit, but remove them from the
-- effective rule set and configuration page.
UPDATE "RiskRule"
SET "status" = 'deleted'
WHERE "code" IN (
'DUPLICATE_PHONE_RATIO',
'ILLEGAL_PHONE_RATIO',
'BLACKLIST_HIT_RATIO',
'TEMPLATE_VARIABLE_ANOMALY'
);
UPDATE "RiskRule"
SET "status" = 'deleted'
WHERE "tenantId" IS NOT NULL AND "applicationId" IS NULL;
DROP INDEX IF EXISTS "RiskRule_tenantId_code_key";
DROP INDEX IF EXISTS "RiskRule_tenantId_status_priority_idx";
CREATE UNIQUE INDEX "RiskRule_applicationId_code_key"
ON "RiskRule"("applicationId", "code");
CREATE UNIQUE INDEX "RiskRule_global_code_key"
ON "RiskRule"("code")
WHERE "applicationId" IS NULL AND "status" <> 'deleted';
CREATE INDEX "RiskRule_tenantId_applicationId_status_priority_idx"
ON "RiskRule"("tenantId", "applicationId", "status", "priority");
ALTER TABLE "RiskRule"
ADD CONSTRAINT "RiskRule_applicationId_fkey"
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "SmsApplication" DROP COLUMN "maxPhonesPerTask";
@@ -0,0 +1,38 @@
ALTER TABLE "SmsSubmitRecord"
ADD COLUMN "retryOfSubmitRecordId" TEXT;
CREATE UNIQUE INDEX "SmsSubmitRecord_retryOfSubmitRecordId_key"
ON "SmsSubmitRecord"("retryOfSubmitRecordId");
ALTER TABLE "SmsSubmitRecord"
ADD CONSTRAINT "SmsSubmitRecord_retryOfSubmitRecordId_fkey"
FOREIGN KEY ("retryOfSubmitRecordId") REFERENCES "SmsSubmitRecord"("id")
ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "AccountTransaction"
ADD COLUMN "idempotencyKey" TEXT;
CREATE UNIQUE INDEX "AccountTransaction_idempotencyKey_key"
ON "AccountTransaction"("idempotencyKey");
ALTER TABLE "CmppDownstreamDelivery"
ADD COLUMN "dedupeKey" TEXT;
WITH ranked_receipts AS (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY "messageRecordId", "deliveryType"
ORDER BY "createdAt", id
) AS row_number
FROM "CmppDownstreamDelivery"
WHERE "messageRecordId" IS NOT NULL
AND "deliveryType" = 'receipt'
)
UPDATE "CmppDownstreamDelivery" AS delivery
SET "dedupeKey" = 'receipt:' || delivery."messageRecordId"
FROM ranked_receipts
WHERE delivery.id = ranked_receipts.id
AND ranked_receipts.row_number = 1;
CREATE UNIQUE INDEX "CmppDownstreamDelivery_dedupeKey_key"
ON "CmppDownstreamDelivery"("dedupeKey");
@@ -0,0 +1,37 @@
ALTER TABLE "SmsSubmitRecord"
ADD COLUMN "channelGroupId" TEXT,
ADD COLUMN "channelGroupName" TEXT;
ALTER TABLE "SmsSubmitRecord"
ADD CONSTRAINT "SmsSubmitRecord_channelGroupId_fkey"
FOREIGN KEY ("channelGroupId") REFERENCES "SmsChannelGroup"("id")
ON DELETE SET NULL ON UPDATE CASCADE;
CREATE INDEX "SmsSubmitRecord_channelGroupId_idx"
ON "SmsSubmitRecord"("channelGroupId");
UPDATE "SmsSubmitRecord" submit
SET
"channelGroupId" = matched."groupId",
"channelGroupName" = matched."groupName"
FROM (
SELECT DISTINCT ON (submit_record.id)
submit_record.id,
route."groupId",
channel_group.name AS "groupName"
FROM "SmsSubmitRecord" submit_record
JOIN "SmsMessageRecord" message ON message.id = submit_record."messageRecordId"
JOIN "ChannelRouteRule" route
ON route."tenantId" = message."tenantId"
AND route."applicationId" = message."applicationId"
AND route.carrier = message.carrier
AND route."channelId" IS NULL
AND route.province IS NULL
JOIN "SmsChannelGroupItem" group_item
ON group_item."groupId" = route."groupId"
AND group_item."channelId" = submit_record."channelId"
JOIN "SmsChannelGroup" channel_group ON channel_group.id = route."groupId"
WHERE route.status = 'active'
ORDER BY submit_record.id, route.priority ASC, route."createdAt" ASC
) matched
WHERE submit.id = matched.id;
@@ -0,0 +1,35 @@
ALTER TABLE "ReportMaterialImportBatch"
ADD COLUMN "reviewedById" TEXT,
ADD COLUMN "reviewedAt" TIMESTAMP(3);
CREATE TABLE "ReportMaterialImportItem" (
"id" TEXT NOT NULL,
"batchId" TEXT NOT NULL,
"rowNumber" INTEGER NOT NULL,
"reportType" TEXT NOT NULL,
"operation" TEXT NOT NULL,
"targetId" TEXT,
"status" TEXT NOT NULL DEFAULT 'pending_review',
"payload" JSONB NOT NULL,
"originalSnapshot" JSONB,
"errorMessage" TEXT,
"reviewReason" TEXT,
"reviewedById" TEXT,
"reviewedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ReportMaterialImportItem_pkey" PRIMARY KEY ("id"),
CONSTRAINT "ReportMaterialImportItem_batchId_fkey"
FOREIGN KEY ("batchId") REFERENCES "ReportMaterialImportBatch"("id")
ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE UNIQUE INDEX "ReportMaterialImportItem_batchId_rowNumber_key"
ON "ReportMaterialImportItem"("batchId", "rowNumber");
CREATE INDEX "ReportMaterialImportItem_batchId_status_idx"
ON "ReportMaterialImportItem"("batchId", "status");
CREATE INDEX "ReportMaterialImportItem_reportType_status_createdAt_idx"
ON "ReportMaterialImportItem"("reportType", "status", "createdAt");
CREATE INDEX "ReportMaterialImportItem_targetId_idx"
ON "ReportMaterialImportItem"("targetId");
@@ -0,0 +1,9 @@
CREATE INDEX "RechargeOrder_payMethod_createdAt_idx" ON "RechargeOrder"("payMethod", "createdAt");
CREATE INDEX "SmsApplication_status_createdAt_idx" ON "SmsApplication"("status", "createdAt");
CREATE INDEX "SmsSignature_tenantId_updatedAt_idx" ON "SmsSignature"("tenantId", "updatedAt");
CREATE INDEX "SmsTemplate_tenantId_auditStatus_createdAt_idx" ON "SmsTemplate"("tenantId", "auditStatus", "createdAt");
CREATE INDEX "SmsChannel_status_createdAt_idx" ON "SmsChannel"("status", "createdAt");
CREATE INDEX "ChannelSignatureReportTask_status_createdAt_idx" ON "ChannelSignatureReportTask"("status", "createdAt");
CREATE INDEX "ChannelSignatureReportRecord_createdAt_idx" ON "ChannelSignatureReportRecord"("createdAt");
CREATE INDEX "SmsMessageRecord_queuedAt_idx" ON "SmsMessageRecord"("queuedAt");
CREATE INDEX "SmsUplinkMessage_receivedAt_idx" ON "SmsUplinkMessage"("receivedAt");
@@ -0,0 +1,131 @@
CREATE TABLE "PhoneFrequencyHit" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"ruleId" TEXT,
"ruleCode" TEXT NOT NULL,
"ruleName" TEXT NOT NULL,
"phoneNumber" TEXT NOT NULL,
"thresholdValue" INTEGER NOT NULL,
"actualValue" INTEGER NOT NULL,
"windowStartedAt" TIMESTAMP(3) NOT NULL,
"windowEndsAt" TIMESTAMP(3) NOT NULL,
"generation" INTEGER NOT NULL DEFAULT 0,
"action" TEXT NOT NULL DEFAULT 'block',
"sourceType" TEXT,
"releasedAt" TIMESTAMP(3),
"releasedById" TEXT,
"releaseReason" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PhoneFrequencyHit_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "PhoneFrequencyState" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"ruleId" TEXT,
"ruleCode" TEXT NOT NULL,
"phoneNumber" TEXT NOT NULL,
"windowStartedAt" TIMESTAMP(3) NOT NULL,
"windowEndsAt" TIMESTAMP(3) NOT NULL,
"count" INTEGER NOT NULL DEFAULT 0,
"generation" INTEGER NOT NULL DEFAULT 0,
"activeHitId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PhoneFrequencyState_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "PhoneFrequencyHit_applicationId_ruleCode_phoneNumber_windowStartedAt_generation_key"
ON "PhoneFrequencyHit"("applicationId", "ruleCode", "phoneNumber", "windowStartedAt", "generation");
CREATE INDEX "PhoneFrequencyHit_tenantId_applicationId_createdAt_idx"
ON "PhoneFrequencyHit"("tenantId", "applicationId", "createdAt");
CREATE INDEX "PhoneFrequencyHit_applicationId_phoneNumber_createdAt_idx"
ON "PhoneFrequencyHit"("applicationId", "phoneNumber", "createdAt");
CREATE INDEX "PhoneFrequencyHit_windowEndsAt_releasedAt_idx"
ON "PhoneFrequencyHit"("windowEndsAt", "releasedAt");
CREATE INDEX "PhoneFrequencyHit_ruleId_idx" ON "PhoneFrequencyHit"("ruleId");
CREATE INDEX "PhoneFrequencyHit_releasedById_idx" ON "PhoneFrequencyHit"("releasedById");
CREATE UNIQUE INDEX "PhoneFrequencyState_activeHitId_key" ON "PhoneFrequencyState"("activeHitId");
CREATE UNIQUE INDEX "PhoneFrequencyState_applicationId_ruleCode_phoneNumber_key"
ON "PhoneFrequencyState"("applicationId", "ruleCode", "phoneNumber");
CREATE INDEX "PhoneFrequencyState_tenantId_applicationId_windowEndsAt_idx"
ON "PhoneFrequencyState"("tenantId", "applicationId", "windowEndsAt");
CREATE INDEX "PhoneFrequencyState_ruleId_idx" ON "PhoneFrequencyState"("ruleId");
ALTER TABLE "PhoneFrequencyHit"
ADD CONSTRAINT "PhoneFrequencyHit_tenantId_fkey"
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyHit"
ADD CONSTRAINT "PhoneFrequencyHit_applicationId_fkey"
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyHit"
ADD CONSTRAINT "PhoneFrequencyHit_ruleId_fkey"
FOREIGN KEY ("ruleId") REFERENCES "RiskRule"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyHit"
ADD CONSTRAINT "PhoneFrequencyHit_releasedById_fkey"
FOREIGN KEY ("releasedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyState"
ADD CONSTRAINT "PhoneFrequencyState_tenantId_fkey"
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyState"
ADD CONSTRAINT "PhoneFrequencyState_applicationId_fkey"
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyState"
ADD CONSTRAINT "PhoneFrequencyState_ruleId_fkey"
FOREIGN KEY ("ruleId") REFERENCES "RiskRule"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyState"
ADD CONSTRAINT "PhoneFrequencyState_activeHitId_fkey"
FOREIGN KEY ("activeHitId") REFERENCES "PhoneFrequencyHit"("id") ON DELETE SET NULL ON UPDATE CASCADE;
INSERT INTO "RiskRule" (
"id", "tenantId", "applicationId", "code", "name", "description", "metric",
"thresholdValue", "action", "status", "priority", "config", "createdAt", "updatedAt"
)
SELECT
'default-phone-frequency-24h', NULL, NULL, 'PHONE_FREQUENCY_24H',
'单号码24小时发送频次', '同一企业应用下,单个号码在北京时间自然日内最多允许10条业务短信。',
'phoneFrequencyCount', 10, 'block', 'active', 40,
'{"periodSeconds":86400,"timeZone":"Asia/Shanghai","alignment":"fixed"}'::jsonb,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
WHERE NOT EXISTS (
SELECT 1 FROM "RiskRule"
WHERE "applicationId" IS NULL
AND "code" = 'PHONE_FREQUENCY_24H'
AND "status" <> 'deleted'
);
INSERT INTO "RiskRule" (
"id", "tenantId", "applicationId", "code", "name", "description", "metric",
"thresholdValue", "action", "status", "priority", "config", "createdAt", "updatedAt"
)
SELECT
'default-phone-frequency-5m', NULL, NULL, 'PHONE_FREQUENCY_5M',
'单号码5分钟发送频次', '同一企业应用下,单个号码在固定5分钟周期内最多允许5条业务短信。',
'phoneFrequencyCount', 5, 'block', 'active', 50,
'{"periodSeconds":300,"timeZone":"Asia/Shanghai","alignment":"fixed"}'::jsonb,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
WHERE NOT EXISTS (
SELECT 1 FROM "RiskRule"
WHERE "applicationId" IS NULL
AND "code" = 'PHONE_FREQUENCY_5M'
AND "status" <> 'deleted'
);
@@ -0,0 +1,34 @@
CREATE TABLE "PhoneFrequencyWhitelist" (
"id" TEXT NOT NULL,
"phoneNumber" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'active',
"reason" TEXT NOT NULL,
"remark" TEXT,
"createdById" TEXT NOT NULL,
"updatedById" TEXT NOT NULL,
"deletedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PhoneFrequencyWhitelist_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "PhoneFrequencyWhitelist_phoneNumber_key"
ON "PhoneFrequencyWhitelist"("phoneNumber");
CREATE INDEX "PhoneFrequencyWhitelist_status_updatedAt_idx"
ON "PhoneFrequencyWhitelist"("status", "updatedAt");
CREATE INDEX "PhoneFrequencyWhitelist_createdById_idx"
ON "PhoneFrequencyWhitelist"("createdById");
CREATE INDEX "PhoneFrequencyWhitelist_updatedById_idx"
ON "PhoneFrequencyWhitelist"("updatedById");
ALTER TABLE "PhoneFrequencyWhitelist"
ADD CONSTRAINT "PhoneFrequencyWhitelist_createdById_fkey"
FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyWhitelist"
ADD CONSTRAINT "PhoneFrequencyWhitelist_updatedById_fkey"
FOREIGN KEY ("updatedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,34 @@
CREATE TABLE "DrainageDetectionRule" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"category" TEXT NOT NULL,
"pattern" TEXT NOT NULL,
"flags" TEXT NOT NULL DEFAULT 'giu',
"priority" INTEGER NOT NULL DEFAULT 100,
"status" TEXT NOT NULL DEFAULT 'active',
"description" TEXT,
"version" INTEGER NOT NULL DEFAULT 1,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DrainageDetectionRule_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "DrainageDetectionRule_code_key" ON "DrainageDetectionRule"("code");
CREATE INDEX "DrainageDetectionRule_status_priority_idx" ON "DrainageDetectionRule"("status", "priority");
ALTER TABLE "SmsMessageRecord"
ADD COLUMN "hasDrainageContent" BOOLEAN,
ADD COLUMN "drainageDetection" JSONB,
ADD COLUMN "drainageDetectionVersion" TEXT,
ADD COLUMN "drainageEvaluatedAt" TIMESTAMP(3);
CREATE INDEX "SmsMessageRecord_hasDrainageContent_queuedAt_idx"
ON "SmsMessageRecord"("hasDrainageContent", "queuedAt");
INSERT INTO "DrainageDetectionRule"
("id", "code", "name", "category", "pattern", "flags", "priority", "status", "description", "version", "updatedAt")
VALUES
('drainage-rule-url', 'URL', 'URL及裸域名', 'url', $regex$(?:https?:\/\/)?(?:www\.)?(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,24}|(?:\d{1,3}\.){3}\d{1,3})(?::\d{1,5})?(?:\/[^\s,;!?<>]*)?$regex$, 'giu', 10, 'active', '识别协议链接、裸域名、短链接及IP地址链接;邮箱区间由检测器排除', 1, CURRENT_TIMESTAMP),
('drainage-rule-mobile', 'MOBILE', '手机号码', 'mobile', $regex$(?:^|[^0-9])((?:\+?86)?1[3-9][0-9]{9})(?:$|[^0-9])$regex$, 'giu', 20, 'active', '规范化后识别+86、空格、短横线及中文标点拆分手机号', 1, CURRENT_TIMESTAMP),
('drainage-rule-landline', 'LANDLINE', '固定电话号码', 'landline', $regex$(?:^|[^0-9])((?:\+?86)?(?:\(0[0-9]{2,3}\)|0[0-9]{2,3})-?[0-9]{7,8}(?:(?:||ext)[0-9]{1,6})?)(?:$|[^0-9])$regex$, 'giu', 30, 'active', '识别区号括号、分隔符和分机号', 1, CURRENT_TIMESTAMP);
@@ -0,0 +1,23 @@
-- PostgreSQL standard-conforming strings preserve backslashes literally. The initial
-- seed used JavaScript-style escaping, so already-migrated databases need their three
-- built-in patterns normalized to the single backslashes expected by RegExp.
UPDATE "DrainageDetectionRule"
SET
"pattern" = $regex$(?:https?:\/\/)?(?:www\.)?(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,24}|(?:\d{1,3}\.){3}\d{1,3})(?::\d{1,5})?(?:\/[^\s,;!?<>]*)?$regex$,
"version" = "version" + 1,
"updatedAt" = CURRENT_TIMESTAMP
WHERE "code" = 'URL';
UPDATE "DrainageDetectionRule"
SET
"pattern" = $regex$(?:^|[^0-9])((?:\+?86)?1[3-9][0-9]{9})(?:$|[^0-9])$regex$,
"version" = "version" + 1,
"updatedAt" = CURRENT_TIMESTAMP
WHERE "code" = 'MOBILE';
UPDATE "DrainageDetectionRule"
SET
"pattern" = $regex$(?:^|[^0-9])((?:\+?86)?(?:\(0[0-9]{2,3}\)|0[0-9]{2,3})-?[0-9]{7,8}(?:(?:||ext)[0-9]{1,6})?)(?:$|[^0-9])$regex$,
"version" = "version" + 1,
"updatedAt" = CURRENT_TIMESTAMP
WHERE "code" = 'LANDLINE';
@@ -0,0 +1,12 @@
ALTER TABLE "SmsMessageRecord"
ADD COLUMN "cmppRegisteredDelivery" BOOLEAN,
ADD COLUMN "timeoutReceiptQueuedAt" TIMESTAMP(3);
ALTER TABLE "CmppInboundLongMessageSegment"
ADD COLUMN "registeredDelivery" BOOLEAN NOT NULL DEFAULT true;
-- Historical CMPP submissions were accepted before Registered_Delivery was
-- persisted. Preserve their existing receipt-enabled behavior.
UPDATE "SmsMessageRecord"
SET "cmppRegisteredDelivery" = true
WHERE "cmppSubmitSequenceId" IS NOT NULL;
@@ -0,0 +1,43 @@
CREATE TABLE "SmsReceiptAnomaly" (
"id" TEXT NOT NULL,
"anomalyKey" TEXT NOT NULL,
"tenantId" TEXT,
"applicationId" TEXT,
"channelId" TEXT,
"messageRecordId" TEXT,
"submitRecordId" TEXT,
"receiptRecordId" TEXT,
"anomalyType" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"previousStatus" TEXT,
"incomingStatus" TEXT,
"rawStatus" TEXT,
"errorCode" TEXT,
"detail" JSONB,
"occurrenceCount" INTEGER NOT NULL DEFAULT 1,
"firstOccurredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastOccurredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"resolvedAt" TIMESTAMP(3),
"resolutionNote" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SmsReceiptAnomaly_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SmsReceiptAnomaly_anomalyKey_key" ON "SmsReceiptAnomaly"("anomalyKey");
CREATE INDEX "SmsReceiptAnomaly_status_lastOccurredAt_idx" ON "SmsReceiptAnomaly"("status", "lastOccurredAt");
CREATE INDEX "SmsReceiptAnomaly_lastOccurredAt_idx" ON "SmsReceiptAnomaly"("lastOccurredAt");
CREATE INDEX "SmsReceiptAnomaly_anomalyType_lastOccurredAt_idx" ON "SmsReceiptAnomaly"("anomalyType", "lastOccurredAt");
CREATE INDEX "SmsReceiptAnomaly_tenantId_status_lastOccurredAt_idx" ON "SmsReceiptAnomaly"("tenantId", "status", "lastOccurredAt");
CREATE INDEX "SmsReceiptAnomaly_applicationId_status_lastOccurredAt_idx" ON "SmsReceiptAnomaly"("applicationId", "status", "lastOccurredAt");
CREATE INDEX "SmsReceiptAnomaly_channelId_status_lastOccurredAt_idx" ON "SmsReceiptAnomaly"("channelId", "status", "lastOccurredAt");
CREATE INDEX "SmsReceiptAnomaly_messageRecordId_idx" ON "SmsReceiptAnomaly"("messageRecordId");
CREATE INDEX "SmsReceiptAnomaly_submitRecordId_idx" ON "SmsReceiptAnomaly"("submitRecordId");
ALTER TABLE "SmsReceiptAnomaly" ADD CONSTRAINT "SmsReceiptAnomaly_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "SmsReceiptAnomaly" ADD CONSTRAINT "SmsReceiptAnomaly_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "SmsReceiptAnomaly" ADD CONSTRAINT "SmsReceiptAnomaly_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "SmsChannel"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "SmsReceiptAnomaly" ADD CONSTRAINT "SmsReceiptAnomaly_messageRecordId_fkey" FOREIGN KEY ("messageRecordId") REFERENCES "SmsMessageRecord"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "SmsReceiptAnomaly" ADD CONSTRAINT "SmsReceiptAnomaly_submitRecordId_fkey" FOREIGN KEY ("submitRecordId") REFERENCES "SmsSubmitRecord"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "SmsReceiptAnomaly" ADD CONSTRAINT "SmsReceiptAnomaly_receiptRecordId_fkey" FOREIGN KEY ("receiptRecordId") REFERENCES "SmsReceiptRecord"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
ALTER TABLE "ProtocolInteractionLog"
ADD COLUMN "phoneNumber" TEXT;
@@ -0,0 +1,203 @@
ALTER TABLE "SmsChannel"
ADD COLUMN "carriers" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[];
UPDATE "SmsChannel"
SET "carriers" = CASE
WHEN "carrier" = 'mobile' THEN ARRAY['mobile']::TEXT[]
WHEN "carrier" = 'unicom' THEN ARRAY['unicom']::TEXT[]
WHEN "carrier" = 'telecom' THEN ARRAY['telecom']::TEXT[]
WHEN "carrier" = 'all' THEN ARRAY['mobile', 'unicom', 'telecom']::TEXT[]
-- 旧页面和旧发送链对空/未知carrier一直按移动处理,迁移保持原业务语义且保证至少一项。
ELSE ARRAY['mobile']::TEXT[]
END;
ALTER TABLE "SmsChannel"
ADD CONSTRAINT "SmsChannel_carriers_supported_check"
CHECK (
cardinality("carriers") BETWEEN 1 AND 3
AND "carriers" <@ ARRAY['mobile', 'unicom', 'telecom']::TEXT[]
);
ALTER TABLE "ChannelSignatureReportTask"
ADD COLUMN "carrier" TEXT,
ADD COLUMN "approvedAt" TIMESTAMP(3),
ADD COLUMN "approvalScope" TEXT NOT NULL DEFAULT 'legacy_channel';
-- The current approved timestamp is reconstructed from the latest transition
-- into approved. updatedAt is deliberately not used because unrelated edits
-- can change it and would incorrectly restart the grace period.
UPDATE "ChannelSignatureReportTask" task
SET "approvedAt" = approved_record."approvedAt"
FROM (
SELECT "taskId", MAX("createdAt") AS "approvedAt"
FROM "ChannelSignatureReportRecord"
WHERE "statusAfter" = 'approved'
GROUP BY "taskId"
) approved_record
WHERE task.id = approved_record."taskId"
AND task.status = 'approved';
CREATE INDEX "ChannelSignatureReportTask_signatureId_channelId_carrier_idx"
ON "ChannelSignatureReportTask"("signatureId", "channelId", "carrier");
-- 旧索引把运营商排除在唯一维度外,会阻止同一签名/通道建立多运营商事实。
-- 拆成三类条件索引,在升级维度的同时继续保护历史任务和引流任务不重复。
DROP INDEX IF EXISTS "ChannelSignatureReportTask_target_key";
CREATE UNIQUE INDEX "ChannelSignatureReportTask_signature_channel_carrier_key"
ON "ChannelSignatureReportTask"("signatureId", "channelId", "carrier")
WHERE "reportType" = 'signature' AND "carrier" IS NOT NULL AND "drainageItemId" IS NULL;
CREATE UNIQUE INDEX "ChannelSignatureReportTask_legacy_signature_channel_key"
ON "ChannelSignatureReportTask"("signatureId", "channelId")
WHERE "reportType" = 'signature' AND "carrier" IS NULL AND "drainageItemId" IS NULL;
CREATE UNIQUE INDEX "ChannelSignatureReportTask_drainage_target_key"
ON "ChannelSignatureReportTask"("signatureId", "drainageItemId", "channelId")
WHERE "reportType" = 'drainage' AND "drainageItemId" IS NOT NULL;
CREATE TABLE "SignatureRetirementRule" (
"id" TEXT NOT NULL,
"ruleType" TEXT NOT NULL,
"targetId" TEXT,
"targetKey" TEXT NOT NULL DEFAULT '',
"enabled" BOOLEAN NOT NULL DEFAULT true,
"mobileWindowDays" INTEGER NOT NULL DEFAULT 30,
"mobileThreshold" INTEGER NOT NULL DEFAULT 1,
"unicomWindowDays" INTEGER NOT NULL DEFAULT 30,
"unicomThreshold" INTEGER NOT NULL DEFAULT 1,
"telecomWindowDays" INTEGER NOT NULL DEFAULT 30,
"telecomThreshold" INTEGER NOT NULL DEFAULT 1,
"messageTemplate" TEXT,
"version" INTEGER NOT NULL DEFAULT 1,
"createdById" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SignatureRetirementRule_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SignatureRetirementRule_ruleType_targetKey_key" ON "SignatureRetirementRule"("ruleType", "targetKey");
CREATE INDEX "SignatureRetirementRule_ruleType_enabled_idx" ON "SignatureRetirementRule"("ruleType", "enabled");
CREATE TABLE "SignatureRetirementWebhook" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"platform" TEXT NOT NULL,
"urlEncrypted" TEXT NOT NULL,
"urlMasked" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'active',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SignatureRetirementWebhook_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "SignatureRetirementWebhook_status_createdAt_idx" ON "SignatureRetirementWebhook"("status", "createdAt");
CREATE TABLE "SignatureRetirementCycle" (
"id" TEXT NOT NULL,
"dimensionType" TEXT NOT NULL,
"signatureId" TEXT NOT NULL,
"channelId" TEXT,
"channelKey" TEXT NOT NULL DEFAULT '',
"carrier" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'open',
"startedOn" DATE NOT NULL,
"lastDetectedOn" DATE NOT NULL,
"resolvedOn" DATE,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SignatureRetirementCycle_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "SignatureRetirementCycle_dimension_status_idx" ON "SignatureRetirementCycle"("dimensionType", "signatureId", "channelKey", "carrier", "status");
CREATE INDEX "SignatureRetirementCycle_status_lastDetectedOn_idx" ON "SignatureRetirementCycle"("status", "lastDetectedOn");
-- 同一监控维度只能存在一个开放周期,数据库约束用于兜住并发检测实例。
CREATE UNIQUE INDEX "SignatureRetirementCycle_open_dimension_key"
ON "SignatureRetirementCycle"("dimensionType", "signatureId", "channelKey", "carrier")
WHERE "status" = 'open';
CREATE TABLE "SignatureRetirementDetection" (
"id" TEXT NOT NULL,
"detectionDate" DATE NOT NULL,
"dimensionType" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"applicationId" TEXT,
"signatureId" TEXT NOT NULL,
"channelId" TEXT,
"channelKey" TEXT NOT NULL DEFAULT '',
"carrier" TEXT NOT NULL,
"windowDays" INTEGER NOT NULL,
"threshold" INTEGER NOT NULL,
"submittedAttempts" INTEGER NOT NULL DEFAULT 0,
"acceptedBusinessCount" INTEGER NOT NULL DEFAULT 0,
"deliveredBusinessCount" INTEGER NOT NULL DEFAULT 0,
"approvedAt" TIMESTAMP(3) NOT NULL,
"ruleId" TEXT,
"ruleVersion" INTEGER NOT NULL DEFAULT 1,
"status" TEXT NOT NULL,
"cycleId" TEXT,
"suppressed" BOOLEAN NOT NULL DEFAULT false,
"notificationTitle" TEXT,
"notificationContent" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SignatureRetirementDetection_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SignatureRetirementDetection_dimension_key" ON "SignatureRetirementDetection"("detectionDate", "dimensionType", "signatureId", "channelKey", "carrier");
CREATE INDEX "SignatureRetirementDetection_date_type_status_idx" ON "SignatureRetirementDetection"("detectionDate", "dimensionType", "status");
CREATE INDEX "SignatureRetirementDetection_signature_carrier_date_idx" ON "SignatureRetirementDetection"("signatureId", "carrier", "detectionDate");
CREATE INDEX "SignatureRetirementDetection_channel_carrier_date_idx" ON "SignatureRetirementDetection"("channelId", "carrier", "detectionDate");
CREATE TABLE "SignatureRetirementMessage" (
"id" TEXT NOT NULL,
"detectionId" TEXT NOT NULL,
"cycleId" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"isRead" BOOLEAN NOT NULL DEFAULT false,
"suppressed" BOOLEAN NOT NULL DEFAULT false,
"readAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SignatureRetirementMessage_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SignatureRetirementMessage_detectionId_key" ON "SignatureRetirementMessage"("detectionId");
CREATE INDEX "SignatureRetirementMessage_created_read_suppressed_idx" ON "SignatureRetirementMessage"("createdAt", "isRead", "suppressed");
CREATE INDEX "SignatureRetirementMessage_tenant_createdAt_idx" ON "SignatureRetirementMessage"("tenantId", "createdAt");
CREATE TABLE "SignatureRetirementSuppression" (
"id" TEXT NOT NULL,
"dimensionType" TEXT NOT NULL,
"signatureId" TEXT NOT NULL,
"channelId" TEXT,
"channelKey" TEXT NOT NULL DEFAULT '',
"carrier" TEXT NOT NULL,
"mode" TEXT NOT NULL,
"muteUntil" DATE,
"active" BOOLEAN NOT NULL DEFAULT true,
"reason" TEXT,
"operatorId" TEXT,
"cancelledAt" TIMESTAMP(3),
"cancelledById" TEXT,
"cancelReason" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SignatureRetirementSuppression_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SignatureRetirementSuppression_dimension_key" ON "SignatureRetirementSuppression"("dimensionType", "signatureId", "channelKey", "carrier");
CREATE INDEX "SignatureRetirementSuppression_active_muteUntil_idx" ON "SignatureRetirementSuppression"("active", "muteUntil");
CREATE TABLE "SignatureRetirementWebhookDelivery" (
"id" TEXT NOT NULL,
"detectionDate" DATE NOT NULL,
"webhookId" TEXT NOT NULL,
"groupKey" TEXT NOT NULL,
"payload" JSONB NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"attemptCount" INTEGER NOT NULL DEFAULT 0,
"nextRetryAt" TIMESTAMP(3),
"lastHttpStatus" INTEGER,
"lastError" TEXT,
"deliveredAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SignatureRetirementWebhookDelivery_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SignatureRetirementWebhookDelivery_key" ON "SignatureRetirementWebhookDelivery"("webhookId", "detectionDate", "groupKey");
CREATE INDEX "SignatureRetirementWebhookDelivery_status_retry_idx" ON "SignatureRetirementWebhookDelivery"("status", "nextRetryAt");
@@ -0,0 +1,153 @@
-- 最终业务口径不再保留历史人工确认:旧通道级状态按通道能力一次性形成运营商级事实。
-- 已有运营商任务代表更新的事实,必须保留且不得被旧任务覆盖。
WITH legacy_targets AS (
SELECT
legacy.id AS "legacyId",
legacy."tenantId",
legacy."signatureId",
legacy."channelId",
legacy.status,
legacy.reason,
legacy."createdById",
supported.carrier
FROM "ChannelSignatureReportTask" legacy
JOIN "SmsChannel" channel ON channel.id = legacy."channelId"
CROSS JOIN LATERAL unnest(
CASE
WHEN cardinality(channel.carriers) > 0 THEN channel.carriers
WHEN channel.carrier = 'all' THEN ARRAY['mobile', 'unicom', 'telecom']::TEXT[]
WHEN channel.carrier IN ('mobile', 'unicom', 'telecom') THEN ARRAY[channel.carrier]::TEXT[]
ELSE ARRAY['mobile']::TEXT[]
END
) AS supported(carrier)
WHERE legacy."reportType" = 'signature'
AND legacy."drainageItemId" IS NULL
AND legacy.carrier IS NULL
AND legacy."approvalScope" = 'legacy_channel'
), inserted_tasks AS (
INSERT INTO "ChannelSignatureReportTask" (
id,
"tenantId",
"signatureId",
"channelId",
carrier,
"approvedAt",
"approvalScope",
"reportType",
"drainageItemId",
status,
reason,
"createdById",
"createdAt",
"updatedAt"
)
SELECT
'legacy-auto-' || md5(target."legacyId" || ':' || target.carrier),
target."tenantId",
target."signatureId",
target."channelId",
target.carrier,
CASE WHEN target.status = 'approved' THEN CURRENT_TIMESTAMP ELSE NULL END,
'carrier_specific',
'signature',
NULL,
target.status,
target.reason,
target."createdById",
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
FROM legacy_targets target
ON CONFLICT DO NOTHING
RETURNING id, "channelId", status
)
INSERT INTO "ChannelSignatureReportRecord" (
id,
"taskId",
"channelId",
action,
"statusBefore",
"statusAfter",
reason,
"operatorId",
"sourceEntry",
"createdAt"
)
SELECT
'legacy-auto-record-' || md5(task.id),
task.id,
task."channelId",
'legacy_carrier_auto_split',
NULL,
task.status,
'历史通道级任务按通道运营商能力自动转换',
NULL,
'migration',
CURRENT_TIMESTAMP
FROM inserted_tasks task
ON CONFLICT (id) DO NOTHING;
WITH legacy_targets AS (
SELECT
legacy.id AS "legacyId",
legacy."signatureId",
legacy."channelId",
supported.carrier
FROM "ChannelSignatureReportTask" legacy
JOIN "SmsChannel" channel ON channel.id = legacy."channelId"
CROSS JOIN LATERAL unnest(
CASE
WHEN cardinality(channel.carriers) > 0 THEN channel.carriers
WHEN channel.carrier = 'all' THEN ARRAY['mobile', 'unicom', 'telecom']::TEXT[]
WHEN channel.carrier IN ('mobile', 'unicom', 'telecom') THEN ARRAY[channel.carrier]::TEXT[]
ELSE ARRAY['mobile']::TEXT[]
END
) AS supported(carrier)
WHERE legacy."reportType" = 'signature'
AND legacy."drainageItemId" IS NULL
AND legacy.carrier IS NULL
AND legacy."approvalScope" = 'legacy_channel'
), completed_legacy AS (
SELECT target."legacyId"
FROM legacy_targets target
LEFT JOIN "ChannelSignatureReportTask" exact
ON exact."signatureId" = target."signatureId"
AND exact."channelId" = target."channelId"
AND exact."reportType" = 'signature'
AND exact."drainageItemId" IS NULL
AND exact.carrier = target.carrier
GROUP BY target."legacyId"
HAVING COUNT(DISTINCT target.carrier) = COUNT(DISTINCT exact.carrier)
), updated_legacy AS (
UPDATE "ChannelSignatureReportTask" legacy
SET "approvalScope" = 'legacy_split',
"updatedAt" = CURRENT_TIMESTAMP
FROM completed_legacy completed
WHERE legacy.id = completed."legacyId"
AND legacy."approvalScope" = 'legacy_channel'
RETURNING legacy.id, legacy."channelId", legacy.status
)
INSERT INTO "ChannelSignatureReportRecord" (
id,
"taskId",
"channelId",
action,
"statusBefore",
"statusAfter",
reason,
"operatorId",
"sourceEntry",
"createdAt"
)
SELECT
'legacy-split-record-' || md5(legacy.id),
legacy.id,
legacy."channelId",
'legacy_scope_auto_split',
legacy.status,
legacy.status,
'全部适用运营商已自动形成独立报备任务',
NULL,
'migration',
CURRENT_TIMESTAMP
FROM updated_legacy legacy
ON CONFLICT (id) DO NOTHING;
@@ -0,0 +1,57 @@
CREATE TABLE "DownstreamRequeueTask" (
"id" TEXT NOT NULL,
"taskNo" TEXT NOT NULL,
"tenantId" TEXT,
"applicationId" TEXT,
"status" TEXT NOT NULL DEFAULT 'queued',
"filterSnapshot" JSONB NOT NULL,
"snapshotAt" TIMESTAMP(3) NOT NULL,
"reason" TEXT NOT NULL,
"ratePerSecond" INTEGER NOT NULL DEFAULT 10,
"consecutiveFailureLimit" INTEGER NOT NULL DEFAULT 10,
"totalCount" INTEGER NOT NULL DEFAULT 0,
"successCount" INTEGER NOT NULL DEFAULT 0,
"failedCount" INTEGER NOT NULL DEFAULT 0,
"skippedCount" INTEGER NOT NULL DEFAULT 0,
"waitingCount" INTEGER NOT NULL DEFAULT 0,
"consecutiveFailures" INTEGER NOT NULL DEFAULT 0,
"lastError" TEXT,
"createdById" TEXT,
"startedAt" TIMESTAMP(3),
"pausedAt" TIMESTAMP(3),
"finishedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DownstreamRequeueTask_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "DownstreamRequeueTaskItem" (
"id" TEXT NOT NULL,
"taskId" TEXT NOT NULL,
"deliveryId" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'queued',
"previousStatus" TEXT NOT NULL,
"skipReason" TEXT,
"errorMessage" TEXT,
"claimedAt" TIMESTAMP(3),
"completedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DownstreamRequeueTaskItem_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "DownstreamRequeueTask_taskNo_key" ON "DownstreamRequeueTask"("taskNo");
CREATE INDEX "DownstreamRequeueTask_status_createdAt_idx" ON "DownstreamRequeueTask"("status", "createdAt");
CREATE INDEX "DownstreamRequeueTask_applicationId_status_createdAt_idx" ON "DownstreamRequeueTask"("applicationId", "status", "createdAt");
CREATE INDEX "DownstreamRequeueTask_tenantId_createdAt_idx" ON "DownstreamRequeueTask"("tenantId", "createdAt");
CREATE UNIQUE INDEX "DownstreamRequeueTaskItem_taskId_deliveryId_key" ON "DownstreamRequeueTaskItem"("taskId", "deliveryId");
CREATE INDEX "DownstreamRequeueTaskItem_taskId_status_createdAt_idx" ON "DownstreamRequeueTaskItem"("taskId", "status", "createdAt");
CREATE INDEX "DownstreamRequeueTaskItem_applicationId_status_createdAt_idx" ON "DownstreamRequeueTaskItem"("applicationId", "status", "createdAt");
CREATE INDEX "DownstreamRequeueTaskItem_deliveryId_status_idx" ON "DownstreamRequeueTaskItem"("deliveryId", "status");
ALTER TABLE "DownstreamRequeueTask" ADD CONSTRAINT "DownstreamRequeueTask_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "DownstreamRequeueTask" ADD CONSTRAINT "DownstreamRequeueTask_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "DownstreamRequeueTask" ADD CONSTRAINT "DownstreamRequeueTask_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "DownstreamRequeueTaskItem" ADD CONSTRAINT "DownstreamRequeueTaskItem_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "DownstreamRequeueTask"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "DownstreamRequeueTaskItem" ADD CONSTRAINT "DownstreamRequeueTaskItem_deliveryId_fkey" FOREIGN KEY ("deliveryId") REFERENCES "CmppDownstreamDelivery"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,19 @@
ALTER TABLE "DownstreamRequeueTask"
ADD COLUMN "applicationFailures" JSONB NOT NULL DEFAULT '{}',
ADD COLUMN "scanLeaseOwner" TEXT,
ADD COLUMN "scanLeaseUntil" TIMESTAMP(3);
CREATE TABLE "DownstreamRequeueRateWindow" (
"id" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"windowStartedAt" TIMESTAMP(3) NOT NULL,
"consumed" INTEGER NOT NULL DEFAULT 0,
"updatedAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "DownstreamRequeueRateWindow_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "DownstreamRequeueRateWindow_applicationId_windowStartedAt_key"
ON "DownstreamRequeueRateWindow"("applicationId", "windowStartedAt");
CREATE INDEX "DownstreamRequeueRateWindow_windowStartedAt_idx"
ON "DownstreamRequeueRateWindow"("windowStartedAt");
@@ -0,0 +1,80 @@
CREATE TABLE "SecurityDetectionRule" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"sourceType" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"threshold" INTEGER NOT NULL,
"windowSeconds" INTEGER NOT NULL,
"cooldownSeconds" INTEGER NOT NULL,
"severity" TEXT NOT NULL,
"defaultBlockSeconds" INTEGER NOT NULL,
"maximumBlockSeconds" INTEGER NOT NULL,
"configVersion" INTEGER NOT NULL DEFAULT 1,
"effectiveVersion" INTEGER NOT NULL DEFAULT 0,
"applyStatus" TEXT NOT NULL DEFAULT 'pending',
"lastApplyError" TEXT,
"pendingConfig" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SecurityDetectionRule_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "SecurityDetectionEvent" (
"id" TEXT NOT NULL, "eventKey" TEXT NOT NULL, "ruleId" TEXT NOT NULL,
"sourceIp" TEXT NOT NULL, "sourcePort" INTEGER, "accountHash" TEXT,
"path" TEXT, "protocol" TEXT, "resultCode" TEXT, "evidence" JSONB,
"occurredAt" TIMESTAMP(3) NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SecurityDetectionEvent_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "SecurityAlert" (
"id" TEXT NOT NULL, "fingerprint" TEXT NOT NULL, "ruleId" TEXT NOT NULL,
"sourceIp" TEXT NOT NULL, "severity" TEXT NOT NULL, "status" TEXT NOT NULL DEFAULT 'open',
"eventCount" INTEGER NOT NULL DEFAULT 0, "windowStartedAt" TIMESTAMP(3) NOT NULL,
"firstOccurredAt" TIMESTAMP(3) NOT NULL, "lastOccurredAt" TIMESTAMP(3) NOT NULL,
"acknowledgedAt" TIMESTAMP(3), "acknowledgedById" TEXT, "ignoredAt" TIMESTAMP(3),
"ignoredById" TEXT, "ignoreReason" TEXT, "blockId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SecurityAlert_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "SecurityBlock" (
"id" TEXT NOT NULL, "operationKey" TEXT NOT NULL, "alertId" TEXT, "sourceIp" TEXT NOT NULL,
"executor" TEXT NOT NULL, "status" TEXT NOT NULL DEFAULT 'requested', "durationSeconds" INTEGER NOT NULL,
"reason" TEXT NOT NULL, "requestedById" TEXT NOT NULL, "requestedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"appliedAt" TIMESTAMP(3), "expiresAt" TIMESTAMP(3), "releasedAt" TIMESTAMP(3), "releasedById" TEXT,
"executorReference" TEXT, "lastError" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "SecurityBlock_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "SecurityProtectedNetwork" (
"id" TEXT NOT NULL, "network" TEXT NOT NULL, "name" TEXT NOT NULL, "reason" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT true, "createdById" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SecurityProtectedNetwork_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SecurityDetectionRule_code_key" ON "SecurityDetectionRule"("code");
CREATE INDEX "SecurityDetectionRule_enabled_sourceType_idx" ON "SecurityDetectionRule"("enabled", "sourceType");
CREATE UNIQUE INDEX "SecurityDetectionEvent_eventKey_key" ON "SecurityDetectionEvent"("eventKey");
CREATE INDEX "SecurityDetectionEvent_ruleId_occurredAt_idx" ON "SecurityDetectionEvent"("ruleId", "occurredAt");
CREATE INDEX "SecurityDetectionEvent_sourceIp_occurredAt_idx" ON "SecurityDetectionEvent"("sourceIp", "occurredAt");
CREATE UNIQUE INDEX "SecurityAlert_fingerprint_key" ON "SecurityAlert"("fingerprint");
CREATE INDEX "SecurityAlert_status_severity_lastOccurredAt_idx" ON "SecurityAlert"("status", "severity", "lastOccurredAt");
CREATE INDEX "SecurityAlert_sourceIp_status_lastOccurredAt_idx" ON "SecurityAlert"("sourceIp", "status", "lastOccurredAt");
CREATE INDEX "SecurityAlert_ruleId_status_lastOccurredAt_idx" ON "SecurityAlert"("ruleId", "status", "lastOccurredAt");
CREATE UNIQUE INDEX "SecurityBlock_operationKey_key" ON "SecurityBlock"("operationKey");
CREATE INDEX "SecurityBlock_status_expiresAt_idx" ON "SecurityBlock"("status", "expiresAt");
CREATE INDEX "SecurityBlock_sourceIp_status_requestedAt_idx" ON "SecurityBlock"("sourceIp", "status", "requestedAt");
CREATE INDEX "SecurityBlock_alertId_idx" ON "SecurityBlock"("alertId");
CREATE UNIQUE INDEX "SecurityProtectedNetwork_network_key" ON "SecurityProtectedNetwork"("network");
CREATE INDEX "SecurityProtectedNetwork_enabled_createdAt_idx" ON "SecurityProtectedNetwork"("enabled", "createdAt");
ALTER TABLE "SecurityDetectionEvent" ADD CONSTRAINT "SecurityDetectionEvent_ruleId_fkey" FOREIGN KEY ("ruleId") REFERENCES "SecurityDetectionRule"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "SecurityAlert" ADD CONSTRAINT "SecurityAlert_ruleId_fkey" FOREIGN KEY ("ruleId") REFERENCES "SecurityDetectionRule"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
INSERT INTO "SecurityDetectionRule" ("id", "code", "name", "sourceType", "threshold", "windowSeconds", "cooldownSeconds", "severity", "defaultBlockSeconds", "maximumBlockSeconds", "configVersion", "effectiveVersion", "applyStatus", "updatedAt") VALUES
('sec_admin_login', 'admin_login_failure', '运营端登录失败', 'application', 8, 600, 900, 'medium', 3600, 604800, 1, 1, 'effective', CURRENT_TIMESTAMP),
('sec_client_login', 'client_login_failure', '客户端登录失败', 'application', 8, 600, 900, 'medium', 3600, 604800, 1, 1, 'effective', CURRENT_TIMESTAMP),
('sec_ssh_auth', 'ssh_auth_failure', 'SSH认证失败', 'fail2ban', 6, 600, 1800, 'high', 86400, 604800, 1, 1, 'effective', CURRENT_TIMESTAMP),
('sec_cmpp_auth', 'cmpp_auth_failure', 'CMPP认证失败', 'gateway', 5, 300, 900, 'high', 3600, 604800, 1, 1, 'effective', CURRENT_TIMESTAMP),
('sec_cmpp_abuse', 'cmpp_protocol_abuse', 'CMPP协议滥用', 'gateway', 20, 60, 900, 'critical', 3600, 604800, 1, 1, 'effective', CURRENT_TIMESTAMP),
('sec_http_key', 'http_invalid_api_key', 'HTTP错误密钥', 'application', 10, 300, 900, 'high', 3600, 604800, 1, 1, 'effective', CURRENT_TIMESTAMP),
('sec_http_sign', 'http_signature_failure', 'HTTP签名错误', 'application', 10, 300, 900, 'high', 3600, 604800, 1, 1, 'effective', CURRENT_TIMESTAMP),
('sec_http_replay', 'http_replay_attempt', 'HTTP重放尝试', 'application', 3, 600, 1800, 'critical', 86400, 604800, 1, 1, 'effective', CURRENT_TIMESTAMP),
('sec_http_scan', 'http_malicious_scan', 'HTTP恶意扫描', 'fail2ban', 20, 60, 900, 'high', 3600, 604800, 1, 1, 'effective', CURRENT_TIMESTAMP);
@@ -0,0 +1,23 @@
CREATE TABLE "InfrastructureAlertSetting" (
"id" TEXT NOT NULL DEFAULT 'global',
"configVersion" INTEGER NOT NULL DEFAULT 1,
"effectiveVersion" INTEGER NOT NULL DEFAULT 1,
"thresholds" JSONB NOT NULL,
"effectiveThresholds" JSONB NOT NULL,
"applyStatus" TEXT NOT NULL DEFAULT 'effective',
"lastError" TEXT,
"updatedById" TEXT,
"appliedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "InfrastructureAlertSetting_pkey" PRIMARY KEY ("id")
);
INSERT INTO "InfrastructureAlertSetting" (
"id", "thresholds", "effectiveThresholds", "appliedAt"
) VALUES (
'global',
'{"hostCpu":{"warning":80,"critical":90},"hostMemory":{"warning":85,"critical":95},"hostDisk":{"warning":80,"critical":90},"apiError":{"warning":1,"critical":5},"apiLatency":{"warning":1,"critical":3},"apiEventLoop":{"warning":0.2,"critical":1},"gatewayQueue":{"warning":30,"critical":120},"postgresConnections":{"warning":70,"critical":85},"redisMemory":{"warning":70,"critical":85},"minioCapacity":{"warning":80,"critical":90}}'::jsonb,
'{"hostCpu":{"warning":80,"critical":90},"hostMemory":{"warning":85,"critical":95},"hostDisk":{"warning":80,"critical":90},"apiError":{"warning":1,"critical":5},"apiLatency":{"warning":1,"critical":3},"apiEventLoop":{"warning":0.2,"critical":1},"gatewayQueue":{"warning":30,"critical":120},"postgresConnections":{"warning":70,"critical":85},"redisMemory":{"warning":70,"critical":85},"minioCapacity":{"warning":80,"critical":90}}'::jsonb,
CURRENT_TIMESTAMP
);
@@ -0,0 +1,17 @@
CREATE TABLE "InfrastructureAlertRead" (
"id" TEXT NOT NULL,
"fingerprint" TEXT NOT NULL,
"activeAt" TIMESTAMP(3) NOT NULL,
"userId" TEXT NOT NULL,
"readAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "InfrastructureAlertRead_pkey" PRIMARY KEY ("id"),
CONSTRAINT "InfrastructureAlertRead_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE UNIQUE INDEX "InfrastructureAlertRead_fingerprint_userId_key"
ON "InfrastructureAlertRead"("fingerprint", "userId");
CREATE INDEX "InfrastructureAlertRead_userId_readAt_idx"
ON "InfrastructureAlertRead"("userId", "readAt");
@@ -0,0 +1,6 @@
ALTER TABLE "SmsSubmitRecord"
ADD COLUMN "resultEventId" TEXT,
ADD COLUMN "resultProcessedAt" TIMESTAMP(3);
CREATE UNIQUE INDEX "SmsSubmitRecord_resultEventId_key"
ON "SmsSubmitRecord"("resultEventId");
@@ -0,0 +1,91 @@
CREATE TABLE "CmppInboundSubmissionInbox" (
"id" TEXT NOT NULL,
"requestKey" TEXT NOT NULL,
"payloadHash" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"queuePriority" TEXT NOT NULL DEFAULT 'normal',
"payload" JSONB NOT NULL,
"response" JSONB NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"attempts" INTEGER NOT NULL DEFAULT 0,
"nextAttemptAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lockedAt" TIMESTAMP(3),
"lockedBy" TEXT,
"lastError" TEXT,
"result" JSONB,
"completedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CmppInboundSubmissionInbox_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "CmppInboundSubmissionInbox_requestKey_key"
ON "CmppInboundSubmissionInbox"("requestKey");
CREATE INDEX "CmppInboundSubmissionInbox_status_nextAttemptAt_createdAt_idx"
ON "CmppInboundSubmissionInbox"("status", "nextAttemptAt", "createdAt");
CREATE INDEX "CmppInboundSubmissionInbox_status_lockedAt_idx"
ON "CmppInboundSubmissionInbox"("status", "lockedAt");
CREATE INDEX "CmppInboundSubmissionInbox_applicationId_createdAt_idx"
ON "CmppInboundSubmissionInbox"("applicationId", "createdAt");
CREATE INDEX "CmppInboundSubmissionInbox_queuePriority_status_createdAt_idx"
ON "CmppInboundSubmissionInbox"("queuePriority", "status", "createdAt");
ALTER TABLE "CmppInboundSubmissionInbox"
ADD CONSTRAINT "CmppInboundSubmissionInbox_tenantId_fkey"
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "CmppInboundSubmissionInbox"
ADD CONSTRAINT "CmppInboundSubmissionInbox_applicationId_fkey"
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
CREATE TABLE "SmsApplicationDailyReservation" (
"id" TEXT NOT NULL,
"reservationKey" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"usageDate" DATE NOT NULL,
"requestedCount" INTEGER NOT NULL,
"dailyLimit" INTEGER NOT NULL,
"usedCount" INTEGER,
"reserved" BOOLEAN NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SmsApplicationDailyReservation_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "SmsApplicationDailyReservation_reservationKey_key"
ON "SmsApplicationDailyReservation"("reservationKey");
CREATE INDEX "SmsApplicationDailyReservation_applicationId_usageDate_idx"
ON "SmsApplicationDailyReservation"("applicationId", "usageDate");
CREATE INDEX "SmsApplicationDailyReservation_tenantId_createdAt_idx"
ON "SmsApplicationDailyReservation"("tenantId", "createdAt");
ALTER TABLE "SmsApplicationDailyReservation" ADD CONSTRAINT "SmsApplicationDailyReservation_tenantId_fkey"
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "SmsApplicationDailyReservation" ADD CONSTRAINT "SmsApplicationDailyReservation_applicationId_fkey"
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
CREATE TABLE "PhoneFrequencyReservation" (
"id" TEXT NOT NULL,
"reservationKey" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"applicationId" TEXT NOT NULL,
"result" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PhoneFrequencyReservation_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "PhoneFrequencyReservation_reservationKey_key"
ON "PhoneFrequencyReservation"("reservationKey");
CREATE INDEX "PhoneFrequencyReservation_applicationId_createdAt_idx"
ON "PhoneFrequencyReservation"("applicationId", "createdAt");
CREATE INDEX "PhoneFrequencyReservation_tenantId_createdAt_idx"
ON "PhoneFrequencyReservation"("tenantId", "createdAt");
ALTER TABLE "PhoneFrequencyReservation" ADD CONSTRAINT "PhoneFrequencyReservation_tenantId_fkey"
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "PhoneFrequencyReservation" ADD CONSTRAINT "PhoneFrequencyReservation_applicationId_fkey"
FOREIGN KEY ("applicationId") REFERENCES "SmsApplication"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

Some files were not shown because too many files have changed in this diff Show More