Merge branch 'yongxu-dev' into devlop
# Conflicts: # miniprogram/pages/profile-edit/profile-edit.js # miniprogram/pages/profile-edit/profile-edit.wxml # miniprogram/pages/settings/settings.js # miniprogram/utils/ruleEngine.js # soul-admin/src/pages/distribution/DistributionPage.tsx # soul-admin/src/pages/users/UsersPage.tsx # soul-api/.env.production # soul-api/.gitignore # soul-api/internal/handler/db_ckb_leads.go # soul-api/internal/handler/miniprogram.go # soul-api/internal/handler/referral.go # 开发文档/1、需求/archive/链接人与事-存客宝同步-需求规划.md # 开发文档/1、需求/archive/链接人与事-实现方案.md
This commit is contained in:
@@ -1,62 +0,0 @@
|
||||
# 文章 base64 图片迁移脚本
|
||||
|
||||
将 `chapters` 表中 `content` 字段内嵌的 base64 图片提取为独立文件,并替换为 `/uploads/book-images/xxx` 的 URL,减小文章体积。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 历史文章中有大量粘贴的 base64 图片
|
||||
- 保存时因 content 过大导致超时或失败
|
||||
- 需要将 base64 转为文件存储
|
||||
|
||||
## 执行方式
|
||||
|
||||
### 1. 测试环境(建议先执行)
|
||||
|
||||
```bash
|
||||
cd soul-api
|
||||
|
||||
# 加载测试环境配置(.env.development)
|
||||
$env:APP_ENV="development"
|
||||
|
||||
# 先 dry-run 预览,不写入
|
||||
go run ./cmd/migrate-base64-images --dry-run
|
||||
|
||||
# 确认无误后正式执行
|
||||
go run ./cmd/migrate-base64-images
|
||||
```
|
||||
|
||||
### 2. 生产环境
|
||||
|
||||
```bash
|
||||
cd soul-api
|
||||
$env:APP_ENV="production"
|
||||
go run ./cmd/migrate-base64-images --dry-run # 先预览
|
||||
go run ./cmd/migrate-base64-images # 正式执行
|
||||
```
|
||||
|
||||
### 3. 指定 DSN(覆盖 .env)
|
||||
|
||||
```bash
|
||||
$env:DB_DSN="user:pass@tcp(host:port)/db?charset=utf8mb4&parseTime=True"
|
||||
go run ./cmd/migrate-base64-images --dry-run
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `--dry-run` | 仅统计和预览,不写入文件与数据库 |
|
||||
|
||||
## 行为说明
|
||||
|
||||
1. 查询 `content LIKE '%data:image%'` 的章节
|
||||
2. 用正则提取 `src="data:image/xxx;base64,..."` 或 `src='...'`
|
||||
3. 解码 base64,保存到 `uploads/book-images/{timestamp}_{random}.{ext}`
|
||||
4. 将 content 中的 base64 src 替换为 `/uploads/book-images/xxx`
|
||||
5. 更新数据库
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **务必先在测试环境验证**,确认无误后再跑生产
|
||||
- 脚本依赖 `UPLOAD_DIR` 或默认 `uploads` 目录
|
||||
- 图片格式支持:png、jpeg、jpg、gif、webp
|
||||
@@ -1,96 +0,0 @@
|
||||
# 数据库与 Go Model 字段对照检查报告
|
||||
|
||||
> 后端工程师对照 `soul-api/internal/model` 与 `soul_miniprogram.sql` 建表结构,列出**数据库表里可能缺失、但代码里在用**的字段。
|
||||
> 若当前库是由旧版 SQL 导入或从未执行过迁移脚本,按本报告执行 `sync-users-vip-and-schema.sql` 或依赖 AutoMigrate 即可补全。
|
||||
|
||||
---
|
||||
|
||||
## 一、结论摘要
|
||||
|
||||
| 表名 | 是否缺字段 | 缺失字段(Model 有、SQL 无) | 影响 |
|
||||
|------|------------|-----------------------------|------|
|
||||
| **users** | 是(旧库可能缺) | is_vip, vip_expire_date, vip_activated_at, vip_sort, vip_role | 订单列表、用户列表、VIP 设置、提现、匹配记录等接口报错(不含 vip_name/vip_avatar 等,小程序已改为直接读用户资料) |
|
||||
| **chapters** | 是(SQL 导出无此列) | hot_score | 文章排名、热门章节等依赖热度分的接口报错 |
|
||||
| 其他业务表 | 否 | - | 与当前 SQL 一致 |
|
||||
|
||||
---
|
||||
|
||||
## 二、users 表
|
||||
|
||||
- **Model 文件**:`internal/model/user.go`
|
||||
- **SQL 表**:`soul_miniprogram.sql` 中 `CREATE TABLE users` 已包含 VIP 相关列;若你的库是**更早的备份**或**未导入最新 SQL**,可能缺少以下列。
|
||||
|
||||
| 列名(蛇形) | 类型 | 说明 |
|
||||
|-------------|------|------|
|
||||
| is_vip | TINYINT(1) NULL DEFAULT 0 | 是否 VIP |
|
||||
| vip_expire_date | DATETIME(3) NULL | VIP 到期时间 |
|
||||
| vip_activated_at | DATETIME(3) NULL | 成为 VIP 时间,排序用 |
|
||||
| vip_sort | INT NULL | 手动排序,越小越前 |
|
||||
| vip_role | VARCHAR(50) NULL | 角色:从 vip_roles 选或手动填写 |
|
||||
|
||||
(vip_name、vip_avatar、vip_project、vip_contact、vip_bio 不再在迁移中新增,小程序已改为直接读用户资料 nickname/avatar/projectIntro/phone 等;已有库可保留该五列作兼容。)
|
||||
|
||||
**修复**:执行 `scripts/sync-users-vip-and-schema.sql` 中 users 部分,或重启 soul-api(未设 `SKIP_AUTO_MIGRATE` 时 AutoMigrate 会补列)。
|
||||
|
||||
---
|
||||
|
||||
## 三、chapters 表
|
||||
|
||||
- **Model 文件**:`internal/model/chapter.go`
|
||||
- **SQL 表**:当前 `soul_miniprogram.sql` 中 `chapters` 仅有 `hot_score_override`(decimal),**没有** `hot_score`(int)。
|
||||
Model 使用的是 `hot_score`(热度分,用于排名算法),因此仅按该 SQL 建表时,数据库**缺少** `hot_score`。
|
||||
|
||||
| 列名(蛇形) | 类型 | 说明 |
|
||||
|-------------|------|------|
|
||||
| hot_score | INT NOT NULL DEFAULT 0 | 热度分,用于排名算法 |
|
||||
|
||||
**修复**:执行 `scripts/sync-users-vip-and-schema.sql` 中 chapters 部分,或执行 `scripts/add-hot-score.sql`,或依赖 soul-api 启动时对 Chapter 的 AutoMigrate。
|
||||
|
||||
---
|
||||
|
||||
## 四、已核对无缺列的表
|
||||
|
||||
以下表在 `soul_miniprogram.sql` 中的列与对应 Model 一致,**无需补列**(仅列名与类型一致即可,顺序可不同):
|
||||
|
||||
- **orders**:与 `model.Order` 一致
|
||||
- **withdrawals**:与 `model.Withdrawal` 一致(库中多出的 transaction_id、error_message 不影响)
|
||||
- **admin_users**:与 `model.AdminUser` 一致
|
||||
- **system_config**:与 `model.SystemConfig` 一致
|
||||
- **referral_bindings**:Model 字段在表中均存在
|
||||
- **referral_visits**:与 `model.ReferralVisit` 一致
|
||||
- **user_rules**:与 `model.UserRule` 一致
|
||||
- **user_tracks**:与 `model.UserTrack` 一致
|
||||
- **reading_progress**:与 `model.ReadingProgress` 一致(表为 section_id,Model 为 section_id)
|
||||
- **match_records**:与 `model.MatchRecord` 一致
|
||||
- **mentor_consultations**:与 `model.MentorConsultation` 一致
|
||||
- **mentors**:与 `model.Mentor` 一致
|
||||
- **link_tags**:与 `model.LinkTag` 一致
|
||||
- **persons**:与 `model.Person` 一致
|
||||
- **author_config**:与 `model.AuthorConfig` 一致
|
||||
- **ckb_lead_records**:与 `model.CkbLeadRecord` 一致
|
||||
- **ckb_submit_records**:与 `model.CkbSubmitRecord` 一致
|
||||
- **user_addresses**:与 `model.UserAddress` 一致
|
||||
- **vip_roles**:与 `model.VipRole` 一致
|
||||
- **wechat_callback_logs**:与 `model.WechatCallbackLog` 一致
|
||||
|
||||
---
|
||||
|
||||
## 五、推荐操作
|
||||
|
||||
1. **一次性补全(推荐)**
|
||||
在备份后执行:
|
||||
```bash
|
||||
mysql -u 用户 -p 数据库名 < soul-api/scripts/sync-users-vip-and-schema.sql
|
||||
```
|
||||
若某条报 `Duplicate column name`,表示该列已存在,可跳过。
|
||||
|
||||
2. **依赖 AutoMigrate**
|
||||
确保 soul-api 的 `database.Init` 中已对 `User`、`SystemConfig`、`Chapter` 执行 `AutoMigrate`,且未设置 `SKIP_AUTO_MIGRATE`,重启服务后会自动补全缺失列。
|
||||
|
||||
3. **新建库**
|
||||
若从零建库,建议用**最新**的 `soul_miniprogram.sql` 导入后,再执行一次 `sync-users-vip-and-schema.sql`,确保 users 与 chapters 与 Model 完全一致。
|
||||
|
||||
---
|
||||
|
||||
**检查日期**:按代码与 SQL 导出时点生成
|
||||
**检查范围**:soul-api 全部 `internal/model` 与 soul_miniprogram.sql 中对应表结构
|
||||
Binary file not shown.
@@ -1,18 +0,0 @@
|
||||
-- 后台管理员用户表(与 soul-api 管理端鉴权配合)
|
||||
CREATE TABLE IF NOT EXISTS admin_users (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(64) NOT NULL,
|
||||
password_hash VARCHAR(128) NOT NULL,
|
||||
role VARCHAR(32) NOT NULL DEFAULT 'admin',
|
||||
name VARCHAR(64) DEFAULT '',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME DEFAULT NULL,
|
||||
UNIQUE KEY uk_admin_users_username (username),
|
||||
KEY idx_admin_users_deleted_at (deleted_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 初始超级管理员需通过 soul-api 启动时从 ADMIN_USERNAME/ADMIN_PASSWORD 自动迁移
|
||||
-- 或执行以下命令生成 bcrypt 哈希后插入(示例密码 admin123):
|
||||
-- 在 Go 中: hash, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
|
||||
@@ -1,23 +0,0 @@
|
||||
-- 作者详情独立表,每个字段单独列,便于编辑与保存
|
||||
-- 执行:mysql -u user -p db < soul-api/scripts/add-author-config-table.sql
|
||||
|
||||
CREATE TABLE IF NOT EXISTS author_config (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(80) NOT NULL DEFAULT '卡若',
|
||||
avatar VARCHAR(4) NOT NULL DEFAULT 'K',
|
||||
avatar_img VARCHAR(500) NOT NULL DEFAULT '',
|
||||
title VARCHAR(200) NOT NULL DEFAULT '',
|
||||
bio TEXT,
|
||||
stats TEXT COMMENT 'JSON: [{"label":"","value":""}]',
|
||||
highlights TEXT COMMENT 'JSON: ["a","b"]',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 插入默认一行(仅当表为空时)
|
||||
INSERT INTO author_config (name, avatar, avatar_img, title, bio, stats, highlights)
|
||||
SELECT '卡若', 'K', '', 'Soul派对房主理人 · 私域运营专家',
|
||||
'每天早上6点到9点,在Soul派对房分享真实的创业故事。专注私域运营与项目变现,用云阿米巴模式帮助创业者构建可持续的商业体系。',
|
||||
'[{"label":"商业案例","value":"62"},{"label":"连续直播","value":"365天"},{"label":"派对分享","value":"1000+"}]',
|
||||
'["5年私域运营经验","帮助100+品牌从0到1增长","连续创业者,擅长商业模式设计"]'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM author_config LIMIT 1);
|
||||
@@ -1,28 +0,0 @@
|
||||
-- 余额相关表(新版迁移)
|
||||
-- 执行:mysql -u user -p database < soul-api/scripts/add-balance-tables.sql
|
||||
|
||||
-- user_balances
|
||||
CREATE TABLE IF NOT EXISTS user_balances (
|
||||
user_id VARCHAR(50) PRIMARY KEY,
|
||||
balance DECIMAL(10,2) DEFAULT 0,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- balance_transactions
|
||||
CREATE TABLE IF NOT EXISTS balance_transactions (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
user_id VARCHAR(50) NOT NULL,
|
||||
type VARCHAR(20) NOT NULL,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
order_id VARCHAR(50) DEFAULT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_user_created (user_id, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- orders 增加 payment_method
|
||||
SET @col_exists = (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE table_schema = DATABASE() AND table_name = 'orders' AND column_name = 'payment_method');
|
||||
SET @sql = IF(@col_exists = 0, 'ALTER TABLE orders ADD COLUMN payment_method VARCHAR(20) DEFAULT NULL AFTER referrer_id', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -1,9 +0,0 @@
|
||||
-- ============================================================
|
||||
-- 章节归属字段 - chapters 表(普通版/增值版)
|
||||
-- 用途:添加文章时区分该章节属于普通版还是增值版
|
||||
-- 执行:mysql -u user -p database < soul-api/scripts/add-chapter-edition-fields.sql
|
||||
-- ============================================================
|
||||
|
||||
-- 新增字段(若列已存在会报错,可忽略)
|
||||
ALTER TABLE chapters ADD COLUMN edition_standard TINYINT(1) NULL DEFAULT 1 COMMENT '是否属于普通版,1=是';
|
||||
ALTER TABLE chapters ADD COLUMN edition_premium TINYINT(1) NULL DEFAULT 0 COMMENT '是否属于增值版,1=是';
|
||||
@@ -1,5 +0,0 @@
|
||||
-- 为 all-chapters 接口加速:sort_order + id 排序索引
|
||||
-- 执行:node .cursor/scripts/db-exec/run.js -f soul-api/scripts/add-chapters-index-for-all-chapters.sql
|
||||
-- 若索引已存在会报错,可忽略
|
||||
|
||||
CREATE INDEX idx_chapters_sort_id ON chapters(sort_order, id);
|
||||
@@ -1,8 +0,0 @@
|
||||
-- ============================================================
|
||||
-- stitch_soul P0:chapters 表新增 is_new 字段
|
||||
-- 用途:目录/首页「最新新增」标识,管理端可勾选
|
||||
-- 执行前请先备份数据库!
|
||||
-- ============================================================
|
||||
|
||||
-- 新增 is_new 字段(若列已存在会报 Duplicate column name,可忽略)
|
||||
ALTER TABLE chapters ADD COLUMN is_new TINYINT(1) NULL DEFAULT 0 COMMENT '是否标记为最新新增';
|
||||
@@ -1,2 +0,0 @@
|
||||
-- 仅添加 ckb_plan_id(若 add-persons-ckb-fields.sql 已部分执行或需单独补列)
|
||||
ALTER TABLE `persons` ADD COLUMN `ckb_plan_id` BIGINT NOT NULL DEFAULT 0 COMMENT '存客宝获客计划ID';
|
||||
@@ -1,6 +0,0 @@
|
||||
-- 代付逻辑改造:gift_pay_requests 增加 quantity、redeemed_count
|
||||
-- 执行:mysql -u user -p db < soul-api/scripts/add-gift-pay-quantity.sql
|
||||
-- 若列已存在会报 Duplicate column,可忽略
|
||||
|
||||
ALTER TABLE gift_pay_requests ADD COLUMN quantity INT NOT NULL DEFAULT 1;
|
||||
ALTER TABLE gift_pay_requests ADD COLUMN redeemed_count INT NOT NULL DEFAULT 0;
|
||||
@@ -1,25 +0,0 @@
|
||||
-- 代付请求表 + 订单表代付字段
|
||||
-- 执行:mysql -u user -p db < soul-api/scripts/add-gift-pay-requests.sql
|
||||
-- 注:orders 表新增字段由 GORM AutoMigrate 自动添加;若需手动执行:
|
||||
-- ALTER TABLE orders ADD COLUMN gift_pay_request_id VARCHAR(50) DEFAULT NULL;
|
||||
-- ALTER TABLE orders ADD COLUMN payer_user_id VARCHAR(50) DEFAULT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gift_pay_requests (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
request_sn VARCHAR(32) NOT NULL UNIQUE,
|
||||
initiator_user_id VARCHAR(50) NOT NULL,
|
||||
product_type VARCHAR(30) NOT NULL,
|
||||
product_id VARCHAR(50) NOT NULL DEFAULT '',
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
description VARCHAR(200) NOT NULL DEFAULT '',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
payer_user_id VARCHAR(50) DEFAULT NULL,
|
||||
order_id VARCHAR(50) DEFAULT NULL,
|
||||
expire_at DATETIME NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_initiator (initiator_user_id),
|
||||
INDEX idx_payer (payer_user_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_request_sn (request_sn)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- 为 chapters 表新增 hot_score 字段(热度分,用于排名算法)
|
||||
ALTER TABLE chapters ADD COLUMN hot_score INT NOT NULL DEFAULT 0;
|
||||
@@ -1,40 +0,0 @@
|
||||
-- stitch_soul 导师与预约表
|
||||
-- 执行:mysql -u user -p db < soul-api/scripts/add-mentors.sql
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `mentors` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`avatar` varchar(500) DEFAULT '',
|
||||
`name` varchar(80) NOT NULL,
|
||||
`intro` varchar(500) DEFAULT '',
|
||||
`tags` varchar(500) DEFAULT '',
|
||||
`price_single` decimal(10,2) DEFAULT NULL,
|
||||
`price_half_year` decimal(10,2) DEFAULT NULL,
|
||||
`price_year` decimal(10,2) DEFAULT NULL,
|
||||
`quote` varchar(500) DEFAULT '',
|
||||
`why_find` text,
|
||||
`offering` text,
|
||||
`judgment_style` varchar(500) DEFAULT '',
|
||||
`sort` int DEFAULT 0,
|
||||
`enabled` tinyint(1) DEFAULT 1,
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_enabled_sort` (`enabled`,`sort`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `mentor_consultations` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NOT NULL,
|
||||
`mentor_id` int NOT NULL,
|
||||
`consultation_type` varchar(20) NOT NULL,
|
||||
`amount` decimal(10,2) NOT NULL,
|
||||
`status` varchar(20) DEFAULT 'created',
|
||||
`order_id` int DEFAULT NULL,
|
||||
`scheduled_at` datetime DEFAULT NULL,
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_mentor_id` (`mentor_id`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -1,5 +0,0 @@
|
||||
-- 订单表新增退款原因列
|
||||
-- 执行: mysql -u user -p db < soul-api/scripts/add-order-refund-reason.sql
|
||||
|
||||
-- 若列已存在(如由 GORM AutoMigrate 创建)会报错,可忽略
|
||||
ALTER TABLE orders ADD COLUMN refund_reason VARCHAR(500) DEFAULT NULL COMMENT '退款原因' AFTER referrer_id;
|
||||
@@ -1,15 +0,0 @@
|
||||
-- orders 表索引优化:提升 /api/orders 列表、营收统计查询性能
|
||||
-- 执行:mysql -u user -p database < soul-api/scripts/add-orders-indexes.sql
|
||||
-- 幂等:索引已存在时跳过,可重复执行
|
||||
|
||||
-- 1. idx_status_created 复合索引
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'orders' AND index_name = 'idx_status_created';
|
||||
SET @sql = IF(@cnt = 0, 'ALTER TABLE orders ADD INDEX idx_status_created (status, created_at)', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 2. idx_user_id 索引
|
||||
SELECT COUNT(*) INTO @cnt FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'orders' AND index_name = 'idx_user_id';
|
||||
SET @sql = IF(@cnt = 0, 'ALTER TABLE orders ADD INDEX idx_user_id (user_id)', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
@@ -1,6 +0,0 @@
|
||||
-- persons 表新增 ckb_api_key 字段
|
||||
-- 作用:存储该 @人物 在存客宝的接入密钥,点击加好友时用该 Key 推线索;留空则 fallback 全局 CKB_LEAD_API_KEY
|
||||
-- 执行:mysql -u user -p db < soul-api/scripts/add-persons-ckb-api-key.sql
|
||||
|
||||
ALTER TABLE persons
|
||||
ADD COLUMN ckb_api_key VARCHAR(100) NOT NULL DEFAULT '' AFTER label;
|
||||
@@ -1,12 +0,0 @@
|
||||
-- 为 persons 表增加存客宝 API 获客相关字段,便于管理端回显与二次编辑
|
||||
ALTER TABLE `persons`
|
||||
ADD COLUMN `greeting` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '存客宝打招呼语' AFTER `ckb_api_key`,
|
||||
ADD COLUMN `tips` TEXT NULL COMMENT '获客成功提示' AFTER `greeting`,
|
||||
ADD COLUMN `remark_type` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '备注类型phone/nickname/source' AFTER `tips`,
|
||||
ADD COLUMN `remark_format` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '备注格式' AFTER `remark_type`,
|
||||
ADD COLUMN `add_friend_interval` INT NOT NULL DEFAULT 1 COMMENT '添加好友间隔(分钟)' AFTER `remark_format`,
|
||||
ADD COLUMN `start_time` VARCHAR(10) NOT NULL DEFAULT '09:00' COMMENT '允许加人开始时间 HH:MM' AFTER `add_friend_interval`,
|
||||
ADD COLUMN `end_time` VARCHAR(10) NOT NULL DEFAULT '18:00' COMMENT '允许加人结束时间 HH:MM' AFTER `start_time`,
|
||||
ADD COLUMN `device_groups` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '参与计划的设备ID列表,逗号分隔' AFTER `end_time`,
|
||||
ADD COLUMN `ckb_plan_id` BIGINT NOT NULL DEFAULT 0 COMMENT '存客宝获客计划ID' AFTER `device_groups`;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
-- persons、link_tags 表,供 ContentPage @提及人物与链接标签配置
|
||||
-- 执行:mysql -u user -p db < soul-api/scripts/add-persons-link-tags.sql
|
||||
|
||||
CREATE TABLE IF NOT EXISTS persons (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
person_id VARCHAR(50) NOT NULL UNIQUE,
|
||||
name VARCHAR(100) NOT NULL DEFAULT '',
|
||||
label VARCHAR(200) NOT NULL DEFAULT '',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS link_tags (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
tag_id VARCHAR(50) NOT NULL UNIQUE,
|
||||
label VARCHAR(200) NOT NULL DEFAULT '',
|
||||
url VARCHAR(500) NOT NULL DEFAULT '',
|
||||
type VARCHAR(20) NOT NULL DEFAULT 'url',
|
||||
app_id VARCHAR(100) NOT NULL DEFAULT '',
|
||||
page_path VARCHAR(500) NOT NULL DEFAULT '',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -1,6 +0,0 @@
|
||||
-- persons 表新增 token 字段(32 位唯一,@ 时存此值,小程序用此兑换 ckb_api_key)
|
||||
-- 执行:cd 项目根目录 && node .cursor/scripts/db-exec/run.js -f soul-api/scripts/add-persons-token.sql
|
||||
-- 或:mysql -u user -p db < soul-api/scripts/add-persons-token.sql
|
||||
|
||||
ALTER TABLE persons ADD COLUMN token VARCHAR(36) NOT NULL DEFAULT '' AFTER person_id;
|
||||
ALTER TABLE persons ADD UNIQUE INDEX idx_persons_token (token);
|
||||
@@ -1,10 +0,0 @@
|
||||
-- persons 表新增 user_id:用于“超级个体开通后自动创建@人”等幂等绑定
|
||||
-- 说明:
|
||||
-- - 允许为空(历史数据/手工创建不绑定 user)
|
||||
-- - 允许多条 NULL(MySQL UNIQUE 对 NULL 不冲突)
|
||||
-- - 绑定后建议一人仅一条 Person(满足“昵称同名@人”需求)
|
||||
|
||||
ALTER TABLE persons
|
||||
ADD COLUMN user_id VARCHAR(50) DEFAULT NULL COMMENT '绑定用户ID(幂等创建@人)',
|
||||
ADD UNIQUE KEY uk_persons_user_id (user_id);
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
-- 为 chapters 表添加 sort_order 列(支持拖拽排序)
|
||||
-- 执行:cd 项目根目录 && node .cursor/scripts/db-exec/run.js -f soul-api/scripts/add-sort-order-to-chapters.sql
|
||||
-- 若 sort_order 已存在会报错,可忽略
|
||||
|
||||
ALTER TABLE chapters ADD COLUMN sort_order INT DEFAULT 0;
|
||||
@@ -1,15 +0,0 @@
|
||||
-- P3 资料扩展:users 表新增个人资料字段(stitch_soul)
|
||||
-- 用于 comprehensive_profile_editor、enhanced_professional_profile
|
||||
|
||||
ALTER TABLE users ADD COLUMN mbti VARCHAR(16) NULL COMMENT 'MBTI类型' AFTER wechat_id;
|
||||
ALTER TABLE users ADD COLUMN region VARCHAR(100) NULL COMMENT '地区' AFTER mbti;
|
||||
ALTER TABLE users ADD COLUMN industry VARCHAR(100) NULL COMMENT '行业' AFTER region;
|
||||
ALTER TABLE users ADD COLUMN position VARCHAR(100) NULL COMMENT '职位' AFTER industry;
|
||||
ALTER TABLE users ADD COLUMN business_scale VARCHAR(100) NULL COMMENT '业务体量' AFTER position;
|
||||
ALTER TABLE users ADD COLUMN skills VARCHAR(500) NULL COMMENT '我擅长' AFTER business_scale;
|
||||
ALTER TABLE users ADD COLUMN story_best_month TEXT NULL COMMENT '最赚钱的一个月' AFTER skills;
|
||||
ALTER TABLE users ADD COLUMN story_achievement TEXT NULL COMMENT '最有成就感的事' AFTER story_best_month;
|
||||
ALTER TABLE users ADD COLUMN story_turning TEXT NULL COMMENT '人生转折点' AFTER story_achievement;
|
||||
ALTER TABLE users ADD COLUMN help_offer VARCHAR(500) NULL COMMENT '我能帮助大家什么' AFTER story_turning;
|
||||
ALTER TABLE users ADD COLUMN help_need VARCHAR(500) NULL COMMENT '我需要什么帮助' AFTER help_offer;
|
||||
ALTER TABLE users ADD COLUMN project_intro TEXT NULL COMMENT '项目介绍' AFTER help_need;
|
||||
@@ -1,8 +0,0 @@
|
||||
-- 规则引擎默认数据:插入「注册」规则,供登录后完善头像引导
|
||||
-- 执行:mysql -u user -p db < soul-api/scripts/add-user-rules-default.sql
|
||||
-- 幂等:若已存在 trigger='注册' 则跳过
|
||||
|
||||
INSERT INTO user_rules (title, description, `trigger`, sort, enabled, created_at, updated_at)
|
||||
SELECT '完善个人信息', '设置头像和昵称,让其他创业者更容易认识你', '注册', 1, 1, NOW(), NOW()
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM user_rules WHERE `trigger` = '注册' LIMIT 1);
|
||||
@@ -1,5 +0,0 @@
|
||||
-- 用户软删除:管理端假删除,用户再次登录会新建账号
|
||||
-- 执行后,DELETE 操作改为 SET deleted_at,不再物理删除,避免外键约束
|
||||
|
||||
ALTER TABLE users ADD COLUMN deleted_at DATETIME(3) NULL DEFAULT NULL COMMENT '软删除时间' AFTER updated_at;
|
||||
CREATE INDEX idx_users_deleted_at ON users (deleted_at);
|
||||
@@ -1,13 +0,0 @@
|
||||
-- 新增 users.vip_activated_at:成为 VIP 时间,用于排序(后付款/后设置在前)
|
||||
-- 执行:mysql -u user -p database < add-vip-activated-at.sql
|
||||
-- 若列已存在会报错,可忽略
|
||||
|
||||
ALTER TABLE users ADD COLUMN vip_activated_at DATETIME NULL COMMENT '成为VIP时间,付款=pay_time,手动=now,排序用';
|
||||
|
||||
-- 可选:为已有 VIP 用户回填 vip_activated_at(取该用户最近一次 vip 订单的 pay_time)
|
||||
-- UPDATE users u
|
||||
-- SET u.vip_activated_at = (
|
||||
-- SELECT MAX(o.pay_time) FROM orders o
|
||||
-- WHERE o.user_id = u.id AND o.product_type = 'vip' AND o.status = 'paid'
|
||||
-- )
|
||||
-- WHERE u.is_vip = 1 AND u.vip_activated_at IS NULL;
|
||||
@@ -1,20 +0,0 @@
|
||||
-- ============================================================
|
||||
-- 会员资料字段 - users 表
|
||||
-- 用途:VIP 页保存资料、创业老板排行展示(与用户信息 phone/wechat_id 分离)
|
||||
-- 执行前请先备份数据库!
|
||||
-- ============================================================
|
||||
|
||||
-- 1. 检查:查看 users 表是否已有这些列(可选执行)
|
||||
-- SHOW COLUMNS FROM users LIKE 'vip_name';
|
||||
-- SHOW COLUMNS FROM users LIKE 'vip_avatar';
|
||||
-- SHOW COLUMNS FROM users LIKE 'vip_project';
|
||||
-- SHOW COLUMNS FROM users LIKE 'vip_contact';
|
||||
-- SHOW COLUMNS FROM users LIKE 'vip_bio';
|
||||
|
||||
-- 2. 新增会员资料字段(若列已存在会报 Duplicate column name,可忽略该条)
|
||||
-- --------------------------------------------------------
|
||||
ALTER TABLE users ADD COLUMN vip_name VARCHAR(100) NULL COMMENT '会员姓名(创业老板排行)';
|
||||
ALTER TABLE users ADD COLUMN vip_avatar VARCHAR(500) NULL COMMENT '会员头像';
|
||||
ALTER TABLE users ADD COLUMN vip_project VARCHAR(200) NULL COMMENT '项目名称';
|
||||
ALTER TABLE users ADD COLUMN vip_contact VARCHAR(100) NULL COMMENT '会员联系方式(展示用,与 phone/wechat_id 分离)';
|
||||
ALTER TABLE users ADD COLUMN vip_bio TEXT NULL COMMENT '一句话简介';
|
||||
@@ -1,25 +0,0 @@
|
||||
-- 1. 新建 vip_roles 表
|
||||
CREATE TABLE IF NOT EXISTS vip_roles (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL UNIQUE COMMENT '角色名称',
|
||||
sort INT DEFAULT 0 COMMENT '下拉展示顺序,越小越前',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) COMMENT '超级个体固定角色';
|
||||
|
||||
-- 2. 插入默认角色(UNIQUE name 防重复)
|
||||
INSERT IGNORE INTO vip_roles (name, sort) VALUES
|
||||
('创始人', 1),
|
||||
('投资人', 2),
|
||||
('产品经理', 3),
|
||||
('技术负责人', 4),
|
||||
('运营总监', 5),
|
||||
('销售总监', 6),
|
||||
('市场总监', 7),
|
||||
('合伙人', 8),
|
||||
('顾问', 9),
|
||||
('品牌主理人', 10);
|
||||
|
||||
-- 3. users 表新增 vip_sort、vip_role
|
||||
ALTER TABLE users ADD COLUMN vip_sort INT NULL COMMENT '手动排序,越小越前';
|
||||
ALTER TABLE users ADD COLUMN vip_role VARCHAR(50) NULL COMMENT '角色:从 vip_roles 选或手动填写';
|
||||
@@ -1,9 +0,0 @@
|
||||
# 补全 persons.ckb_api_key
|
||||
|
||||
若存在 ckb_plan_id 但 ckb_api_key 为空的 Person,可手动调用 plan/detail 补全。
|
||||
|
||||
**执行前**:确保 soul-api 可连接存客宝(CKB_OPEN_API_KEY、CKB_OPEN_ACCOUNT 已配置)。
|
||||
|
||||
**方式一**:管理端逐个编辑保存(会触发存客宝同步,若 Person 无 ckb_api_key 需在编辑弹窗填写或重新创建)。
|
||||
|
||||
**方式二**:写一次性脚本,遍历 `ckb_plan_id > 0 AND (ckb_api_key IS NULL OR ckb_api_key = '')` 的 Person,调 ckbOpenGetPlanDetail 获取 apiKey 并 UPDATE。
|
||||
@@ -1,5 +0,0 @@
|
||||
-- 修复篇章标题:将 slug 形式的 part_title 更新为展示标题(数据来源:DB)
|
||||
-- 执行:node .cursor/scripts/db-exec/run.js -f soul-api/scripts/fix-part-titles.sql
|
||||
|
||||
-- part-2026-daily 的标题应为「2026每日派对干货」
|
||||
UPDATE chapters SET part_title = '2026每日派对干货' WHERE part_id = 'part-2026-daily' AND (part_title = 'part-2026-daily' OR part_title = '' OR part_title IS NULL);
|
||||
@@ -1,83 +0,0 @@
|
||||
-- ============================================================
|
||||
-- VIP 订单修复脚本
|
||||
-- 场景:甲方开发的 VIP 支付可能未正确设置 product_type
|
||||
-- 会员价:1980 元
|
||||
-- 执行前请先备份数据库!
|
||||
-- ============================================================
|
||||
|
||||
-- 1. 诊断:查看当前疑似 VIP 订单的状态(执行后人工确认)
|
||||
-- --------------------------------------------------------
|
||||
SELECT id, order_sn, user_id, product_type, amount, status, pay_time, created_at
|
||||
FROM orders
|
||||
WHERE amount = 1980
|
||||
AND (status = 'paid' OR status = 'completed')
|
||||
ORDER BY pay_time DESC;
|
||||
|
||||
-- 2. 统计:有多少条需要修复
|
||||
-- --------------------------------------------------------
|
||||
SELECT COUNT(*) AS need_fix_count
|
||||
FROM orders
|
||||
WHERE amount = 1980
|
||||
AND (status = 'paid' OR status = 'completed')
|
||||
AND (product_type IS NULL OR product_type = '' OR product_type NOT IN ('vip', 'fullbook'));
|
||||
|
||||
-- 3. 修复:将 1980 元已支付订单的 product_type 设为 'vip'
|
||||
-- --------------------------------------------------------
|
||||
-- 条件:金额=1980 且 已支付 且 product_type 不是 vip/fullbook
|
||||
UPDATE orders
|
||||
SET product_type = 'vip'
|
||||
WHERE amount = 1980
|
||||
AND (status = 'paid' OR status = 'completed')
|
||||
AND (product_type IS NULL OR product_type = '' OR product_type NOT IN ('vip', 'fullbook'));
|
||||
|
||||
-- 4. 兼容大小写:若 product_type 为 'VIP'、'Vip' 等,统一为小写
|
||||
-- --------------------------------------------------------
|
||||
UPDATE orders
|
||||
SET product_type = 'vip'
|
||||
WHERE amount = 1980
|
||||
AND (status = 'paid' OR status = 'completed')
|
||||
AND LOWER(TRIM(product_type)) = 'vip'
|
||||
AND product_type != 'vip';
|
||||
|
||||
-- 5. 验证:修复后应无遗漏
|
||||
-- --------------------------------------------------------
|
||||
SELECT id, order_sn, user_id, product_type, amount, status
|
||||
FROM orders
|
||||
WHERE amount = 1980
|
||||
AND (status = 'paid' OR status = 'completed')
|
||||
AND product_type NOT IN ('vip', 'fullbook');
|
||||
-- 期望结果:0 行
|
||||
|
||||
-- ============================================================
|
||||
-- 可选:若线上 next-project 用 users 表存 is_vip,需确保字段存在
|
||||
-- 执行前请确认 users 表是否已有这些列!
|
||||
-- ============================================================
|
||||
|
||||
-- 6. 检查 users 表是否有 VIP 相关列(MySQL)
|
||||
-- SHOW COLUMNS FROM users LIKE 'is_vip';
|
||||
-- SHOW COLUMNS FROM users LIKE 'vip_expire_date';
|
||||
|
||||
-- 7. 若 users 表无 VIP 列,可执行以下 ALTER(按需取消注释)
|
||||
-- --------------------------------------------------------
|
||||
-- ALTER TABLE users ADD COLUMN is_vip TINYINT(1) DEFAULT 0;
|
||||
-- ALTER TABLE users ADD COLUMN vip_expire_date DATETIME NULL;
|
||||
-- ALTER TABLE users ADD COLUMN vip_name VARCHAR(100) NULL;
|
||||
-- ALTER TABLE users ADD COLUMN vip_avatar VARCHAR(500) NULL;
|
||||
-- ALTER TABLE users ADD COLUMN vip_project VARCHAR(200) NULL;
|
||||
-- ALTER TABLE users ADD COLUMN vip_contact VARCHAR(100) NULL;
|
||||
-- ALTER TABLE users ADD COLUMN vip_bio TEXT NULL;
|
||||
|
||||
-- 8. 从 orders 同步到 users(仅当用 users 表存 VIP 时)
|
||||
-- 将 1980 元已支付订单对应的用户标记为 VIP,过期日 = pay_time + 365 天
|
||||
-- --------------------------------------------------------
|
||||
-- UPDATE users u
|
||||
-- INNER JOIN (
|
||||
-- SELECT user_id, MAX(pay_time) AS last_pay
|
||||
-- FROM orders
|
||||
-- WHERE amount = 1980
|
||||
-- AND (status = 'paid' OR status = 'completed')
|
||||
-- AND product_type IN ('vip', 'fullbook')
|
||||
-- GROUP BY user_id
|
||||
-- ) o ON u.id = o.user_id
|
||||
-- SET u.is_vip = 1,
|
||||
-- u.vip_expire_date = DATE_ADD(o.last_pay, INTERVAL 365 DAY);
|
||||
@@ -1,14 +0,0 @@
|
||||
-- 预置用户旅程引导规则(高频行为锚点)
|
||||
-- 执行时幂等:如果规则已存在则忽略
|
||||
|
||||
INSERT IGNORE INTO user_rules (title, description, trigger, sort, enabled, created_at, updated_at) VALUES
|
||||
('注册完成 → 填写头像', '用户完成注册后,引导填写头像和昵称,提升个人信息完整度', '注册', 10, 1, NOW(), NOW()),
|
||||
('完成匹配 → 补充个人资料', '用户完成 Soul 派对房匹配后,引导填写 MBTI、行业、职位等详细信息', '完成匹配', 20, 1, NOW(), NOW()),
|
||||
('首次浏览章节 → 绑定手机号', '用户点击阅读收费章节时,引导绑定手机号以完成身份验证', '点击收费章节', 30, 1, NOW(), NOW()),
|
||||
('付款 ¥1980 → 填写完整信息', '购买全书(1980元)后,需填写:真实姓名、联系方式、所在行业、MBTI,以便进入 VIP 群', '完成付款', 40, 1, NOW(), NOW()),
|
||||
('加入派对房 → 填写项目介绍', '进入 Soul 派对房前,引导填写个人项目介绍和核心需求,便于精准匹配', '加入派对房', 50, 1, NOW(), NOW()),
|
||||
('浏览 5 个章节 → 分享推广', '用户累计阅读 5 个章节后,触发分享引导,邀请好友可获得收益', '累计浏览5章节', 60, 1, NOW(), NOW()),
|
||||
('绑定微信 → 开启分销', '绑定微信后,提示用户开启分销功能,生成专属推广码', '绑定微信', 70, 1, NOW(), NOW()),
|
||||
('收益达到 ¥50 → 申请提现', '累计分销收益超过 50 元时,引导用户申请提现', '收益满50元', 80, 1, NOW(), NOW()),
|
||||
('完善存客宝信息 → 进入流量池', '引导用户授权存客宝信息同步,进入对应微信流量池,获得精准服务', '手动触发', 90, 1, NOW(), NOW()),
|
||||
('浏览导师主页 → 预约咨询', '用户浏览导师详情页超过 30 秒,引导预约一对一咨询', '浏览导师页', 100, 1, NOW(), NOW());
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 订单对账防漏单 - 宝塔定时任务用
|
||||
# 建议每 10 分钟执行一次
|
||||
|
||||
URL="${SYNC_ORDERS_URL:-https://soul.quwanzhi.com/api/cron/sync-orders}"
|
||||
|
||||
curl -s -X GET "$URL" \
|
||||
-H "User-Agent: Baota-Cron/1.0" \
|
||||
--connect-timeout 10 \
|
||||
--max-time 30
|
||||
|
||||
echo ""
|
||||
@@ -1,16 +0,0 @@
|
||||
-- ============================================================
|
||||
-- 同步 users 表与 Go Model(仅 VIP 身份/状态字段,不含单独 VIP 资料列)
|
||||
-- 小程序已改为直接读用户资料(nickname/avatar/projectIntro/phone),不再单独存 vip_name/vip_avatar 等。
|
||||
-- 若某条 ALTER 报 Duplicate column name,说明该列已存在,跳过即可。
|
||||
-- 也可直接重启 soul-api(未设 SKIP_AUTO_MIGRATE 时会自动补列)。
|
||||
-- ============================================================
|
||||
|
||||
-- users 表:VIP 身份与状态(与 internal/model/user.go 一致)
|
||||
ALTER TABLE users ADD COLUMN is_vip TINYINT(1) NULL DEFAULT 0 COMMENT '是否 VIP';
|
||||
ALTER TABLE users ADD COLUMN vip_expire_date DATETIME(3) NULL DEFAULT NULL COMMENT 'VIP 到期时间';
|
||||
ALTER TABLE users ADD COLUMN vip_activated_at DATETIME(3) NULL DEFAULT NULL COMMENT '成为 VIP 时间,排序用';
|
||||
ALTER TABLE users ADD COLUMN vip_sort INT NULL DEFAULT NULL COMMENT '手动排序,越小越前';
|
||||
ALTER TABLE users ADD COLUMN vip_role VARCHAR(50) NULL DEFAULT NULL COMMENT '角色:从 vip_roles 选或手动填写';
|
||||
|
||||
-- chapters 表:Model 使用 hot_score(热度分),SQL 导出里只有 hot_score_override,缺则排名等接口报错
|
||||
ALTER TABLE chapters ADD COLUMN hot_score INT NOT NULL DEFAULT 0 COMMENT '热度分,用于排名算法';
|
||||
@@ -1,61 +0,0 @@
|
||||
# stitch_soul P0 接口验证脚本
|
||||
# 用法:先启动 soul-api,然后执行 .\scripts\test-p0-endpoints.ps1
|
||||
# 可指定 baseUrl:$env:API_BASE = "http://localhost:8080"; .\scripts\test-p0-endpoints.ps1
|
||||
|
||||
$base = if ($env:API_BASE) { $env:API_BASE } else { "http://localhost:8080" }
|
||||
|
||||
Write-Host "=== stitch_soul P0 接口测试 ===" -ForegroundColor Cyan
|
||||
Write-Host "Base: $base`n" -ForegroundColor Gray
|
||||
|
||||
$passed = 0
|
||||
$failed = 0
|
||||
|
||||
function Test-Endpoint {
|
||||
param($name, $path)
|
||||
try {
|
||||
$r = Invoke-RestMethod -Uri "$base$path" -Method GET -TimeoutSec 5
|
||||
if ($r.success -eq $true) {
|
||||
Write-Host "[PASS] $name" -ForegroundColor Green
|
||||
$script:passed++
|
||||
return $r
|
||||
} else {
|
||||
Write-Host "[FAIL] $name - success != true" -ForegroundColor Red
|
||||
$script:failed++
|
||||
}
|
||||
} catch {
|
||||
Write-Host "[FAIL] $name - $($_.Exception.Message)" -ForegroundColor Red
|
||||
$script:failed++
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# 1. book/all-chapters(含 isNew)
|
||||
$r1 = Test-Endpoint "GET /api/miniprogram/book/all-chapters" "/api/miniprogram/book/all-chapters"
|
||||
if ($r1 -and $r1.data) {
|
||||
$first = $r1.data[0]
|
||||
if ($null -ne $first.PSObject.Properties['isNew']) {
|
||||
Write-Host " -> isNew 字段存在" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " -> 警告: isNew 字段可能缺失" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# 2. book/recommended(精选推荐,前3章+tag)
|
||||
$r2 = Test-Endpoint "GET /api/miniprogram/book/recommended" "/api/miniprogram/book/recommended"
|
||||
if ($r2 -and $r2.data) {
|
||||
Write-Host " -> 返回 $($r2.data.Count) 条,tag: $($r2.data[0].tag)" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# 3. book/latest-chapters(最新更新)
|
||||
$r3 = Test-Endpoint "GET /api/miniprogram/book/latest-chapters" "/api/miniprogram/book/latest-chapters"
|
||||
if ($r3 -and $r3.data) {
|
||||
Write-Host " -> 返回 $($r3.data.Count) 条" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# 4. book/hot(热门章节)
|
||||
$r4 = Test-Endpoint "GET /api/miniprogram/book/hot" "/api/miniprogram/book/hot"
|
||||
if ($r4 -and $r4.data) {
|
||||
Write-Host " -> 返回 $($r4.data.Count) 条" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
Write-Host "`n=== 结果: $passed 通过, $failed 失败 ===" -ForegroundColor $(if ($failed -eq 0) { "Green" } else { "Yellow" })
|
||||
@@ -1,93 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
模拟微信「商家转账到零钱」结果通知回调,请求本地/远程回调接口,
|
||||
用于验证:1)接口是否可达 2)wechat_callback_logs 表是否会写入一条记录。
|
||||
|
||||
说明:未使用真实签名与加密,服务端会验签失败并返回 500,
|
||||
但仍会写入 wechat_callback_logs 一条 handler_result=fail 的记录。
|
||||
运行前请确保 soul-api 已启动;运行后请查表 wechat_callback_logs 是否有新行。
|
||||
"""
|
||||
|
||||
import json
|
||||
import ssl
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.error import URLError, HTTPError
|
||||
|
||||
# 默认请求地址(可改环境或命令行)
|
||||
DEFAULT_URL = "http://localhost:8080/api/payment/wechat/transfer/notify"
|
||||
|
||||
|
||||
def main():
|
||||
args = [a for a in sys.argv[1:] if a and not a.startswith("-")]
|
||||
insecure = "--insecure" in sys.argv or "-k" in sys.argv
|
||||
url = args[0] if args else DEFAULT_URL
|
||||
|
||||
if insecure and url.startswith("https://"):
|
||||
print("已启用 --insecure,跳过 SSL 证书校验(仅用于本地/测试)")
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
else:
|
||||
ctx = None
|
||||
|
||||
# 模拟微信回调的请求体结构(真实场景中 resource.ciphertext 为 AEAD_AES_256_GCM 加密,这里用占位)
|
||||
body = {
|
||||
"id": "test-notify-id-" + datetime.now().strftime("%Y%m%d%H%M%S"),
|
||||
"create_time": datetime.now().strftime("%Y-%m-%dT%H:%M:%S+08:00"),
|
||||
"resource_type": "encrypt-resource",
|
||||
"event_type": "MCHTRANSFER.BILL.FINISHED",
|
||||
"summary": "模拟转账结果通知",
|
||||
"resource": {
|
||||
"original_type": "mch_payment",
|
||||
"algorithm": "AEAD_AES_256_GCM",
|
||||
"ciphertext": "fake-base64-ciphertext-for-test",
|
||||
"nonce": "fake-nonce",
|
||||
"associated_data": "mch_payment",
|
||||
},
|
||||
}
|
||||
body_bytes = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Wechatpay-Timestamp": str(int(datetime.now().timestamp())),
|
||||
"Wechatpay-Nonce": "test-nonce-" + datetime.now().strftime("%H%M%S"),
|
||||
"Wechatpay-Signature": "fake-signature-for-test",
|
||||
"Wechatpay-Serial": "fake-serial-for-test",
|
||||
}
|
||||
|
||||
req = Request(url, data=body_bytes, headers=headers, method="POST")
|
||||
|
||||
print(f"POST {url}")
|
||||
print(f"Body (摘要): event_type={body['event_type']}, resource_type={body['resource_type']}")
|
||||
print("-" * 50)
|
||||
|
||||
try:
|
||||
with urlopen(req, timeout=10, context=ctx) as resp:
|
||||
print(f"HTTP 状态: {resp.status}")
|
||||
raw = resp.read().decode("utf-8", errors="replace")
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
print("响应 JSON:", json.dumps(parsed, ensure_ascii=False, indent=2))
|
||||
except Exception:
|
||||
print("响应 body:", raw[:500])
|
||||
except HTTPError as e:
|
||||
print(f"HTTP 状态: {e.code}")
|
||||
raw = e.read().decode("utf-8", errors="replace")
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
print("响应 JSON:", json.dumps(parsed, ensure_ascii=False, indent=2))
|
||||
except Exception:
|
||||
print("响应 body:", raw[:500])
|
||||
except URLError as e:
|
||||
print(f"请求失败: {e.reason}")
|
||||
sys.exit(1)
|
||||
|
||||
print("-" * 50)
|
||||
print("请检查数据库表 wechat_callback_logs 是否有新记录(本次为模拟请求,预期会有一条 handler_result=fail 的记录)。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,64 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
请求提现测试接口:固定用户提现 1 元(默认),无需 admin_session。
|
||||
用法:
|
||||
python test_withdraw.py
|
||||
python test_withdraw.py https://soul.quwanzhi.com
|
||||
python test_withdraw.py http://localhost:8080 2
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.error import URLError, HTTPError
|
||||
from urllib.parse import urlencode
|
||||
|
||||
DEFAULT_BASE = "http://localhost:8080"
|
||||
DEFAULT_USER_ID = "ogpTW5fmXRGNpoUbXB3UEqnVe5Tg"
|
||||
DEFAULT_AMOUNT = "1"
|
||||
|
||||
|
||||
def main():
|
||||
base = DEFAULT_BASE
|
||||
amount = DEFAULT_AMOUNT
|
||||
args = [a for a in sys.argv[1:] if a]
|
||||
if args:
|
||||
if args[0].startswith("http://") or args[0].startswith("https://"):
|
||||
base = args[0].rstrip("/")
|
||||
args = args[1:]
|
||||
if args:
|
||||
amount = args[0]
|
||||
|
||||
path = "/api/withdraw-test"
|
||||
if not base.endswith(path):
|
||||
base = base.rstrip("/") + path
|
||||
url = f"{base}?{urlencode({'userId': DEFAULT_USER_ID, 'amount': amount})}"
|
||||
|
||||
req = Request(url, method="GET")
|
||||
req.add_header("Accept", "application/json")
|
||||
|
||||
print(f"GET {url}")
|
||||
print("-" * 50)
|
||||
|
||||
try:
|
||||
with urlopen(req, timeout=15) as resp:
|
||||
raw = resp.read().decode("utf-8", errors="replace")
|
||||
try:
|
||||
print(json.dumps(json.loads(raw), ensure_ascii=False, indent=2))
|
||||
except Exception:
|
||||
print(raw)
|
||||
except HTTPError as e:
|
||||
raw = e.read().decode("utf-8", errors="replace")
|
||||
try:
|
||||
print(json.dumps(json.loads(raw), ensure_ascii=False, indent=2))
|
||||
except Exception:
|
||||
print(raw)
|
||||
print(f"HTTP {e.code}", file=sys.stderr)
|
||||
except URLError as e:
|
||||
print(f"请求失败: {e.reason}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user