docs: 新增证书说明与数据库迁移文档
- 新增 api/certs/README_wechat_cert.md - 新增 api/migrations/README.md - 新增 api/migrations/sync_schema_to_models.sql Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
29
api/certs/README_wechat_cert.md
Normal file
29
api/certs/README_wechat_cert.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 微信支付 401 签名错误排查
|
||||
|
||||
## 原因
|
||||
|
||||
**WECHAT_PAY_CERT_SERIAL_NO** 必须是「商户 API 证书」的序列号,且该证书与 `apiclient_key.pem`(商户私钥)为一对。
|
||||
若序列号与私钥不对应,微信会返回 401 签名错误。
|
||||
|
||||
## 获取正确序列号
|
||||
|
||||
### 方式一:从商户证书文件读取(推荐)
|
||||
|
||||
若你有 `apiclient_cert.pem`(与 `apiclient_key.pem` 同时从商户平台下载):
|
||||
|
||||
```bash
|
||||
cd api
|
||||
openssl x509 -in certs/apiclient_cert.pem -noout -serial
|
||||
```
|
||||
|
||||
输出示例:`serial=55D97D542DBAA9CB9B7D0ACDB810CC99C208D328`
|
||||
将等号后面的十六进制(可全部大写)填到 `.env` 的 `WECHAT_PAY_CERT_SERIAL_NO`。
|
||||
|
||||
### 方式二:从商户平台查看
|
||||
|
||||
登录 [微信支付商户平台](https://pay.weixin.qq.com) → **账户中心** → **API 安全** → **API 证书** → 查看与当前私钥对应的**证书序列号**,复制到 `WECHAT_PAY_CERT_SERIAL_NO`。
|
||||
|
||||
## 其他可能
|
||||
|
||||
- **私钥与证书不是一对**:重新在商户平台下载 API 证书,使用新的 `apiclient_key.pem` 与证书序列号。
|
||||
- **服务器时间偏差**:与标准时间误差不要超过约 5 分钟。
|
||||
15
api/migrations/README.md
Normal file
15
api/migrations/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# 数据库迁移
|
||||
|
||||
## 说明
|
||||
|
||||
- **`sync_schema_to_models.sql`**:与当前 `api/database/models.py` 保持一致的幂等迁移脚本,**部署工作流会在每次部署后自动执行**。可重复执行,已存在的表/列会跳过。
|
||||
- 其余 `add_*.sql` 为历史增量迁移,可按需单独执行;若已运行过 `sync_schema_to_models.sql`,通常不必再跑。
|
||||
|
||||
## 手动执行
|
||||
|
||||
```bash
|
||||
# 从项目根目录
|
||||
psql -U <user> -d <database> -f api/migrations/sync_schema_to_models.sql
|
||||
```
|
||||
|
||||
若使用与 docker-compose 默认不同的数据库用户/库名,可在 GitHub 仓库中配置 Secrets:`MIGRATION_DB_USER`、`MIGRATION_DB_NAME`,工作流将用其执行迁移。
|
||||
130
api/migrations/sync_schema_to_models.sql
Normal file
130
api/migrations/sync_schema_to_models.sql
Normal file
@@ -0,0 +1,130 @@
|
||||
-- 数据库结构同步迁移脚本(与 api/database/models.py 保持一致)
|
||||
-- 幂等:可重复执行,已存在的表/列会跳过。
|
||||
-- 执行方式: psql -U <user> -d <database> -f api/migrations/sync_schema_to_models.sql
|
||||
-- 执行时间: 2026-02
|
||||
|
||||
-- ========== 1. users 表缺失列 ==========
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'users' AND column_name = 'subscription_type') THEN
|
||||
ALTER TABLE users ADD COLUMN subscription_type VARCHAR DEFAULT 'free';
|
||||
RAISE NOTICE '已添加 users.subscription_type';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'users' AND column_name = 'subscription_expires_at') THEN
|
||||
ALTER TABLE users ADD COLUMN subscription_expires_at TIMESTAMP WITH TIME ZONE DEFAULT NULL;
|
||||
RAISE NOTICE '已添加 users.subscription_expires_at';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'users' AND column_name = 'openid') THEN
|
||||
ALTER TABLE users ADD COLUMN openid VARCHAR UNIQUE;
|
||||
RAISE NOTICE '已添加 users.openid';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ========== 2. refresh_tokens 表缺失列 ==========
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'refresh_tokens' AND column_name = 'device_info') THEN
|
||||
ALTER TABLE refresh_tokens ADD COLUMN device_info VARCHAR;
|
||||
RAISE NOTICE '已添加 refresh_tokens.device_info';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ========== 3. conversations 表缺失列 ==========
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'conversations' AND column_name = 'current_topic') THEN
|
||||
ALTER TABLE conversations ADD COLUMN current_topic VARCHAR;
|
||||
RAISE NOTICE '已添加 conversations.current_topic';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'conversations' AND column_name = 'conversation_stage') THEN
|
||||
ALTER TABLE conversations ADD COLUMN conversation_stage VARCHAR;
|
||||
RAISE NOTICE '已添加 conversations.conversation_stage';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ========== 4. chapters 表缺失列 ==========
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'chapters' AND column_name = 'category') THEN
|
||||
ALTER TABLE chapters ADD COLUMN category VARCHAR;
|
||||
RAISE NOTICE '已添加 chapters.category';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'chapters' AND column_name = 'is_new') THEN
|
||||
ALTER TABLE chapters ADD COLUMN is_new BOOLEAN DEFAULT TRUE;
|
||||
RAISE NOTICE '已添加 chapters.is_new';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'chapters' AND column_name = 'source_segments') THEN
|
||||
ALTER TABLE chapters ADD COLUMN source_segments JSONB;
|
||||
RAISE NOTICE '已添加 chapters.source_segments';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'chapters' AND column_name = 'images') THEN
|
||||
ALTER TABLE chapters ADD COLUMN images JSONB;
|
||||
RAISE NOTICE '已添加 chapters.images';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ========== 5. books 表缺失列 ==========
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'books' AND column_name = 'has_update') THEN
|
||||
ALTER TABLE books ADD COLUMN has_update BOOLEAN DEFAULT FALSE;
|
||||
RAISE NOTICE '已添加 books.has_update';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'books' AND column_name = 'last_update_chapter_id') THEN
|
||||
ALTER TABLE books ADD COLUMN last_update_chapter_id VARCHAR;
|
||||
RAISE NOTICE '已添加 books.last_update_chapter_id';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ========== 6. orders 表(若无则创建) ==========
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id VARCHAR NOT NULL PRIMARY KEY,
|
||||
user_id VARCHAR NOT NULL REFERENCES users(id),
|
||||
plan_id VARCHAR NOT NULL,
|
||||
plan_name VARCHAR NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
currency VARCHAR DEFAULT 'CNY',
|
||||
payment_method VARCHAR NOT NULL,
|
||||
status VARCHAR DEFAULT 'pending',
|
||||
trade_no VARCHAR,
|
||||
paid_at TIMESTAMP WITH TIME ZONE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
expired_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS ix_orders_user_id ON orders(user_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_orders_trade_no ON orders(trade_no);
|
||||
CREATE INDEX IF NOT EXISTS ix_orders_status ON orders(status);
|
||||
|
||||
-- ========== 7. sms_verification_codes 表(若无则创建) ==========
|
||||
CREATE TABLE IF NOT EXISTS sms_verification_codes (
|
||||
id VARCHAR PRIMARY KEY,
|
||||
phone VARCHAR NOT NULL,
|
||||
code VARCHAR NOT NULL,
|
||||
purpose VARCHAR NOT NULL,
|
||||
is_used BOOLEAN DEFAULT FALSE,
|
||||
is_expired BOOLEAN DEFAULT FALSE,
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
verified_at TIMESTAMP WITH TIME ZONE,
|
||||
ip_address VARCHAR
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sms_phone ON sms_verification_codes(phone);
|
||||
CREATE INDEX IF NOT EXISTS idx_sms_created_at ON sms_verification_codes(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_sms_purpose ON sms_verification_codes(purpose);
|
||||
CREATE INDEX IF NOT EXISTS idx_sms_phone_purpose ON sms_verification_codes(phone, purpose);
|
||||
|
||||
-- ========== 8. memoir_states 表(若无则创建,供 create_all 未执行环境使用) ==========
|
||||
CREATE TABLE IF NOT EXISTS memoir_states (
|
||||
id VARCHAR NOT NULL PRIMARY KEY,
|
||||
user_id VARCHAR NOT NULL UNIQUE REFERENCES users(id),
|
||||
stage_order JSONB DEFAULT '[]'::jsonb,
|
||||
current_stage VARCHAR DEFAULT 'childhood',
|
||||
covered_stages JSONB DEFAULT '[]'::jsonb,
|
||||
slots JSONB NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE 'sync_schema_to_models 迁移执行完成';
|
||||
END $$;
|
||||
Reference in New Issue
Block a user