131_affiliate_rebate_hardening.sql 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
  1. -- 1) Normalize historical affiliate rebate rate values.
  2. -- Legacy compatibility treated 0<x<=1 as fractional inputs (e.g. 0.2 => 20%).
  3. -- We now use pure percentage semantics, so convert persisted fractional values once.
  4. UPDATE settings
  5. SET value = to_char((value::numeric * 100), 'FM999999990.########'),
  6. updated_at = NOW()
  7. WHERE key = 'affiliate_rebate_rate'
  8. AND value ~ '^-?[0-9]+(\\.[0-9]+)?$'
  9. AND value::numeric > 0
  10. AND value::numeric <= 1;
  11. -- 2) Affiliate ledger for accrual/transfer traceability.
  12. CREATE TABLE IF NOT EXISTS user_affiliate_ledger (
  13. id BIGSERIAL PRIMARY KEY,
  14. user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  15. action VARCHAR(32) NOT NULL,
  16. amount DECIMAL(20,8) NOT NULL,
  17. source_user_id BIGINT NULL REFERENCES users(id) ON DELETE SET NULL,
  18. created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  19. updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
  20. );
  21. CREATE INDEX IF NOT EXISTS idx_user_affiliate_ledger_user_id ON user_affiliate_ledger(user_id);
  22. CREATE INDEX IF NOT EXISTS idx_user_affiliate_ledger_action ON user_affiliate_ledger(action);
  23. COMMENT ON TABLE user_affiliate_ledger IS '邀请返利资金流水(累计/转入)';
  24. COMMENT ON COLUMN user_affiliate_ledger.action IS 'accrue|transfer';
  25. -- 3) Enforce idempotency at DB layer for payment audit actions.
  26. WITH ranked AS (
  27. SELECT id,
  28. ROW_NUMBER() OVER (PARTITION BY order_id, action ORDER BY id) AS rn
  29. FROM payment_audit_logs
  30. )
  31. DELETE FROM payment_audit_logs p
  32. USING ranked r
  33. WHERE p.id = r.id
  34. AND r.rn > 1;
  35. CREATE UNIQUE INDEX IF NOT EXISTS idx_payment_audit_logs_order_action_uniq
  36. ON payment_audit_logs(order_id, action);
  37. -- 4) Prevent retroactive affiliate rebate issuance for legacy completed balance orders.
  38. INSERT INTO payment_audit_logs (order_id, action, detail, operator, created_at)
  39. SELECT po.id::text,
  40. 'AFFILIATE_REBATE_SKIPPED',
  41. '{"reason":"baseline before affiliate rebate idempotency rollout"}',
  42. 'system',
  43. NOW()
  44. FROM payment_orders po
  45. WHERE po.order_type = 'balance'
  46. AND po.status = 'COMPLETED'
  47. AND NOT EXISTS (
  48. SELECT 1
  49. FROM payment_audit_logs pal
  50. WHERE pal.order_id = po.id::text
  51. AND pal.action IN ('AFFILIATE_REBATE_APPLIED', 'AFFILIATE_REBATE_SKIPPED')
  52. );