Files
bid-doctor/server/migrations/versions/0010_subscription_lifecycle.py
YANG JIANKUAN dd8c04001d
Some checks failed
ci / server (push) Has been cancelled
ci / web (push) Has been cancelled
ci / contract (push) Has been cancelled
feat(m6): 额度流水、套餐开通、数据删除、运营后台(M6 剩下的 P0)
决策 M6-1–M6-10(docs/architecture/m6-plan.md、docs/decisions.md)。

用户端:
- 额度与套餐页:本期额度、按功能分布、扣点规则、流水(逐条余额、关联项目和任务、导出 CSV)
- 开通 / 续费申请(对公转账,运营确认到账后开通),80% / 100% 提醒
- 专业版到期:整个账号只读、额度清零,到期前 30 / 7 / 0 天提醒(worker 每小时 billing_tick)
- 删除项目(彻底删除、销毁数据密钥),注销账号(企业名称 + 短信验证码)
- 通知铃铛、账号页

运营后台:
- 独立进程 bid-doctor ops(/ops/v1)和 apps/admin;用户名 + 密码 + TOTP + IP 白名单
- 开通申请(到账 / 开通 / 退回 / 发票)、企业查询和调额度、误报 / 漏报、失败任务、审计;不看业务内容
- 左侧菜单(标准 logo、可展开收起的分组、图标、待办数、环境标签)
- 运营人员角色只留结构:ops_admin.role、权限点、每个接口的检查点、菜单按权限过滤

数据库:迁移 0010(排队的订阅、申请单号、通知类型、billing_due)、0011(bd_ops 权限)、0012(运营角色)。
另修:取消解析 / 检查接口报 500;uuid7 进程内单调;data_key 销毁后不再新建;审计 None 存 SQL NULL。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:06:06 +08:00

67 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""套餐开通、续费和到期(m6-plan §1.2、§1.3,决策 M6-5):
- subscription 加「scheduled」:续费到账时当前一期还没到期,新一期排队,到旧一期结束时由定时任务生效并发放额度;
- activation_request 加 request_no(界面和转账备注用的申请单号,如 KT-7Q3M2X);
- 通知加 subscription_expired(到期后只读)、activation_rejected(申请被退回);
- billing_due(now):定时任务找到期、该生效、该提醒的订阅。worker 受行级安全策略约束,看不到别的企业,
这里照登录前查询的做法(data-model §2.3)用 bd_auth 所有的 SECURITY DEFINER 函数,只返回编号。
Revision ID: 0010
"""
from __future__ import annotations
from alembic import op
revision = "0010"
down_revision = "0009"
def upgrade() -> None:
op.get_bind().exec_driver_sql("""
ALTER TABLE subscription DROP CONSTRAINT subscription_status_check;
ALTER TABLE subscription ADD CONSTRAINT subscription_status_check
CHECK (status IN ('scheduled', 'active', 'expired', 'cancelled'));
CREATE INDEX ON subscription (tenant_id, starts_at) WHERE status = 'scheduled';
ALTER TABLE activation_request ADD COLUMN request_no text UNIQUE;
ALTER TABLE notification DROP CONSTRAINT notification_kind_check;
ALTER TABLE notification ADD CONSTRAINT notification_kind_check
CHECK (kind IN ('parse_succeeded', 'parse_failed', 'check_succeeded', 'check_failed', 'report_succeeded',
'report_failed', 'quota_80', 'quota_100', 'subscription_activated', 'subscription_expiring',
'subscription_expired', 'activation_rejected'));
CREATE FUNCTION billing_due(p_now timestamptz)
RETURNS TABLE (tenant_id uuid, subscription_id uuid, action text)
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = app AS $$
SELECT s.tenant_id, s.id, 'expire' FROM subscription s
WHERE s.status = 'active' AND s.ends_at IS NOT NULL AND s.ends_at <= p_now
UNION ALL
SELECT s.tenant_id, s.id, 'start' FROM subscription s
WHERE s.status = 'scheduled' AND s.starts_at <= p_now
UNION ALL
SELECT s.tenant_id, s.id, 'remind' FROM subscription s
WHERE s.status = 'active' AND s.ends_at IS NOT NULL AND s.ends_at > p_now AND s.ends_at <= p_now + interval '30 days'
$$;
GRANT SELECT ON subscription TO bd_auth;
ALTER FUNCTION billing_due(timestamptz) OWNER TO bd_auth;
GRANT EXECUTE ON FUNCTION billing_due(timestamptz) TO bd_app;
""")
def downgrade() -> None:
op.get_bind().exec_driver_sql("""
DROP FUNCTION billing_due(timestamptz);
REVOKE SELECT ON subscription FROM bd_auth;
ALTER TABLE notification DROP CONSTRAINT notification_kind_check;
ALTER TABLE notification ADD CONSTRAINT notification_kind_check
CHECK (kind IN ('parse_succeeded', 'parse_failed', 'check_succeeded', 'check_failed', 'report_succeeded',
'report_failed', 'quota_80', 'quota_100', 'subscription_activated', 'subscription_expiring'));
ALTER TABLE activation_request DROP COLUMN request_no;
DROP INDEX IF EXISTS subscription_tenant_id_starts_at_idx;
ALTER TABLE subscription DROP CONSTRAINT subscription_status_check;
ALTER TABLE subscription ADD CONSTRAINT subscription_status_check
CHECK (status IN ('active', 'expired', 'cancelled'));
""")