文件
shanghaipeiwan/server/src/db/mysql.ts
xcbyy 001f81eaca feat(lbs): 大神真实 5km 即时匹配与接单心跳
- companions 新增 latitude/longitude/last_active_at 与 idx_comp_active;切换接单中立即刷新活跃时间

- 新增 /api/companions/heartbeat(鉴权)与 /api/companions/match(真实注册+审核通过+接单中+10分钟心跳,Haversine 5km 过滤与距离排序,不返回精确坐标)

- 工作台/接单大厅/雷达前台每 60s 心跳,离开停止;首页即时匹配改为服务端 LBS 匹配并展示真实距离

- 线上实测:2010 匹配排除自己,仅命中 280m 的 8 号;匿名命中 9 号 10m、8 号 280m;北京 5km 为 0;118 套件/771 用例全绿
2026-10-22 18:44:02 +08:00

1397 行
72 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

import 'dotenv/config';
import mysql, { Pool } from 'mysql2/promise';
export const dbConfig = {
host: process.env.MYSQL_HOST || '127.0.0.1',
port: Number(process.env.MYSQL_PORT) || 3306,
user: process.env.MYSQL_USER || 'root',
password: process.env.MYSQL_PASSWORD || '123456',
database: process.env.MYSQL_DATABASE || 'peiwan',
charset: 'utf8mb4',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
};
export const pool: Pool = mysql.createPool(dbConfig);
// 初始化所有核心数据表与种子数据
export async function initDatabase(): Promise<void> {
const conn = await pool.getConnection();
try {
// 1. 活动表
await conn.query(`
CREATE TABLE IF NOT EXISTS activities (
id VARCHAR(64) PRIMARY KEY,
creator_id INT NOT NULL,
title VARCHAR(255) NOT NULL,
game_category VARCHAR(64) NOT NULL,
location VARCHAR(255),
latitude DECIMAL(10,6) DEFAULT NULL,
longitude DECIMAL(10,6) DEFAULT NULL,
start_time VARCHAR(64) DEFAULT '',
pay_type INT NOT NULL DEFAULT 1,
total_fee_cents INT DEFAULT 0,
per_person_fee_cents INT DEFAULT 0,
max_participants INT DEFAULT 4,
allow_dynamic_price TINYINT DEFAULT 1,
price_adjust_rule VARCHAR(32) DEFAULT 'UNIFIED_HIGH',
status VARCHAR(32) DEFAULT 'RECRUITING',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
try {
await conn.query(`ALTER TABLE activities ADD COLUMN start_time VARCHAR(64) DEFAULT '' AFTER location`);
} catch (_) {
// 字段已存在忽略
}
try {
await conn.query(`ALTER TABLE activities ADD COLUMN latitude DECIMAL(10,6) DEFAULT NULL AFTER location`);
} catch (_) {
// 字段已存在忽略
}
try {
await conn.query(`ALTER TABLE activities ADD COLUMN longitude DECIMAL(10,6) DEFAULT NULL AFTER latitude`);
} catch (_) {
// 字段已存在忽略
}
try {
await conn.query(`ALTER TABLE activities ADD COLUMN status VARCHAR(32) DEFAULT 'RECRUITING' AFTER price_adjust_rule`);
} catch (_) {
// 字段已存在忽略
}
// 2. 参与者与4位纯数字核销码表
await conn.query(`
CREATE TABLE IF NOT EXISTS participants (
id VARCHAR(64) PRIMARY KEY,
activity_id VARCHAR(64) NOT NULL,
user_id INT NOT NULL,
pay_amount_cents INT NOT NULL,
verify_code VARCHAR(4) NOT NULL,
checkin_status TINYINT DEFAULT 0,
verified_at DATETIME NULL,
verified_by INT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_activity (activity_id),
INDEX idx_verify_code (verify_code),
UNIQUE KEY uq_activity_user (activity_id, user_id),
UNIQUE KEY uq_activity_verify (activity_id, verify_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 历史库补丁:为已存在的 participants 表补充唯一约束(防重复报名 / 防核销码冲突)
for (const uqSql of [
'ALTER TABLE participants ADD UNIQUE KEY uq_activity_user (activity_id, user_id)',
'ALTER TABLE participants ADD UNIQUE KEY uq_activity_verify (activity_id, verify_code)'
]) {
try {
await conn.query(uqSql);
} catch (_) {
// 约束已存在或历史数据存在重复,忽略(应用层仍会二次校验)
}
}
// 3. 陪玩大神档案表
await conn.query(`
CREATE TABLE IF NOT EXISTS companions (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(64) NOT NULL,
avatar TEXT,
gender VARCHAR(10),
age INT,
track VARCHAR(64) NOT NULL,
rank_name VARCHAR(64),
hourly_rate INT NOT NULL,
voice_duration INT DEFAULT 15,
voice_title VARCHAR(128),
voice_intro_url VARCHAR(255) DEFAULT '',
rating DECIMAL(3,2) DEFAULT 4.90,
order_count INT DEFAULT 0,
status VARCHAR(32) DEFAULT '接单中',
audit_status VARCHAR(32) DEFAULT '已通过',
id_card_masked VARCHAR(32),
tags JSON,
latitude DECIMAL(10,7) DEFAULT NULL,
longitude DECIMAL(10,7) DEFAULT NULL,
last_active_at DATETIME DEFAULT NULL,
apply_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 动态为 companions 增加 tags 字段、user_id 字段与 reject_reason 字段 (若历史表已存在)
try {
await conn.query('ALTER TABLE companions ADD COLUMN tags JSON AFTER id_card_masked');
} catch (_) {}
try {
await conn.query('ALTER TABLE companions ADD COLUMN user_id INT DEFAULT NULL AFTER id');
} catch (_) {}
try {
await conn.query("ALTER TABLE companions ADD COLUMN reject_reason VARCHAR(255) DEFAULT '' AFTER audit_status");
} catch (_) {}
try {
await conn.query("ALTER TABLE companions ADD COLUMN voice_intro_url VARCHAR(255) NOT NULL DEFAULT '' AFTER voice_title");
} catch (_) {}
try {
await conn.query('ALTER TABLE companions ADD COLUMN latitude DECIMAL(10,7) DEFAULT NULL AFTER tags');
} catch (_) {}
try {
await conn.query('ALTER TABLE companions ADD COLUMN longitude DECIMAL(10,7) DEFAULT NULL AFTER latitude');
} catch (_) {}
try {
await conn.query('ALTER TABLE companions ADD COLUMN last_active_at DATETIME DEFAULT NULL AFTER longitude');
} catch (_) {}
// 4. 订单表
await conn.query(`
CREATE TABLE IF NOT EXISTS orders (
id VARCHAR(64) PRIMARY KEY,
buyer_id INT DEFAULT 1001,
expert_id INT DEFAULT 101,
order_no VARCHAR(64) UNIQUE NOT NULL,
client_name VARCHAR(64) NOT NULL,
companion_name VARCHAR(64) NOT NULL,
expert_avatar TEXT,
track VARCHAR(64) NOT NULL,
service_title VARCHAR(255) DEFAULT '同城陪伴服务',
expense_burden VARCHAR(32) DEFAULT '客户全包',
hourly_rate INT NOT NULL,
duration_hours INT NOT NULL DEFAULT 2,
amount DECIMAL(10,2) NOT NULL,
location VARCHAR(255),
latitude DECIMAL(10,6) DEFAULT NULL,
longitude DECIMAL(10,6) DEFAULT NULL,
verify_code VARCHAR(8),
remark VARCHAR(255),
status VARCHAR(32) NOT NULL DEFAULT '待支付',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
const extraOrderCols = [
"ALTER TABLE orders ADD COLUMN buyer_id INT DEFAULT 1001 AFTER id",
"ALTER TABLE orders ADD COLUMN expert_id INT DEFAULT 101 AFTER buyer_id",
"ALTER TABLE orders ADD COLUMN expert_avatar TEXT AFTER companion_name",
"ALTER TABLE orders ADD COLUMN service_title VARCHAR(255) DEFAULT '同城陪伴服务' AFTER track",
"ALTER TABLE orders ADD COLUMN verify_code VARCHAR(8) AFTER location",
"ALTER TABLE orders ADD COLUMN latitude DECIMAL(10,6) DEFAULT NULL AFTER location",
"ALTER TABLE orders ADD COLUMN longitude DECIMAL(10,6) DEFAULT NULL AFTER latitude",
"ALTER TABLE orders ADD COLUMN remark VARCHAR(255) AFTER verify_code",
"ALTER TABLE orders ADD COLUMN pay_type VARCHAR(32) DEFAULT 'WECHAT' AFTER status",
"ALTER TABLE orders ADD COLUMN coupon_discount DECIMAL(10,2) DEFAULT 0.00 AFTER amount",
"ALTER TABLE orders ADD COLUMN paid_at DATETIME NULL AFTER created_at",
"ALTER TABLE orders ADD COLUMN finish_applied_at DATETIME NULL AFTER paid_at",
"ALTER TABLE orders ADD COLUMN refund_amount DECIMAL(10,2) DEFAULT 0.00 AFTER coupon_discount",
"ALTER TABLE orders ADD COLUMN claimed_by INT NULL AFTER expert_id",
"ALTER TABLE orders ADD COLUMN claimed_at DATETIME NULL AFTER claimed_by",
"ALTER TABLE orders ADD COLUMN user_coupon_id VARCHAR(64) NULL AFTER coupon_discount"
];
for (const colSql of extraOrderCols) {
try {
await conn.query(colSql);
} catch (_) {
// 字段已存在则忽略
}
}
// 5. 争议仲裁表
await conn.query(`
CREATE TABLE IF NOT EXISTS arbitrations (
id VARCHAR(64) PRIMARY KEY,
order_id VARCHAR(64) NOT NULL,
order_no VARCHAR(64) NOT NULL,
client_name VARCHAR(64) NOT NULL,
companion_name VARCHAR(64) NOT NULL,
reason VARCHAR(255) NOT NULL,
user_evidence TEXT,
status VARCHAR(32) NOT NULL DEFAULT '待平台仲裁',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 6. 资金财务流水表
await conn.query(`
CREATE TABLE IF NOT EXISTS finance_records (
id VARCHAR(64) PRIMARY KEY,
record_no VARCHAR(64) UNIQUE NOT NULL,
user_id INT,
type VARCHAR(64) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
direction VARCHAR(8) NOT NULL DEFAULT 'IN',
counterparty VARCHAR(64),
related_id VARCHAR(64),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
try {
await conn.query('ALTER TABLE finance_records ADD COLUMN user_id INT AFTER record_no');
} catch (_) {}
// 7. 提现申请表
await conn.query(`
CREATE TABLE IF NOT EXISTS withdrawals (
id VARCHAR(64) PRIMARY KEY,
withdraw_no VARCHAR(64) UNIQUE NOT NULL,
companion_name VARCHAR(64) NOT NULL,
companion_id INT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
balance DECIMAL(10,2) NOT NULL,
channel VARCHAR(64) DEFAULT '微信商户零钱',
payee_name VARCHAR(64) DEFAULT '',
payee_account VARCHAR(128) DEFAULT '',
status VARCHAR(32) DEFAULT '待审核',
transfer_bill_no VARCHAR(64) DEFAULT '',
transfer_status VARCHAR(32) DEFAULT '',
apply_time DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
try {
await conn.query('ALTER TABLE withdrawals ADD COLUMN reason VARCHAR(255) DEFAULT ""');
} catch (_) {}
try {
await conn.query("ALTER TABLE withdrawals ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP");
} catch (_) {}
try {
await conn.query("ALTER TABLE withdrawals ADD COLUMN transfer_bill_no VARCHAR(64) DEFAULT ''");
} catch (_) {}
try {
await conn.query("ALTER TABLE withdrawals ADD COLUMN transfer_status VARCHAR(32) DEFAULT ''");
} catch (_) {}
try {
await conn.query("ALTER TABLE withdrawals ADD COLUMN payee_name VARCHAR(64) DEFAULT ''");
} catch (_) {}
try {
await conn.query("ALTER TABLE withdrawals ADD COLUMN payee_account VARCHAR(128) DEFAULT ''");
} catch (_) {}
// 7.5 微信支付支付意图与对账表 (幂等入账的唯一依据)
await conn.query(`
CREATE TABLE IF NOT EXISTS payment_intents (
id VARCHAR(64) PRIMARY KEY,
out_trade_no VARCHAR(64) UNIQUE NOT NULL,
scene VARCHAR(32) NOT NULL DEFAULT 'ORDER',
user_id INT NOT NULL DEFAULT 0,
order_id VARCHAR(64) DEFAULT '',
title VARCHAR(128) DEFAULT '',
amount_cents INT NOT NULL DEFAULT 0,
bonus_cents INT NOT NULL DEFAULT 0,
status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
transaction_id VARCHAR(64) DEFAULT '',
credited TINYINT NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
paid_at DATETIME NULL,
INDEX idx_user_scene (user_id, scene, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 7.5.1 微信原路退款记录(幂等:out_refund_no 唯一)
await conn.query(`
CREATE TABLE IF NOT EXISTS payment_refunds (
id INT PRIMARY KEY AUTO_INCREMENT,
out_refund_no VARCHAR(64) UNIQUE NOT NULL,
out_trade_no VARCHAR(64) NOT NULL,
order_id VARCHAR(64) DEFAULT '',
refund_id VARCHAR(64) DEFAULT '',
refund_cents INT NOT NULL DEFAULT 0,
total_cents INT NOT NULL DEFAULT 0,
status VARCHAR(32) NOT NULL DEFAULT 'PROCESSING',
reason VARCHAR(255) DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_order (order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 8. 线下合规场所库
await conn.query(`
CREATE TABLE IF NOT EXISTS safety_places (
id VARCHAR(64) PRIMARY KEY,
name VARCHAR(128) NOT NULL,
area VARCHAR(128) NOT NULL,
category VARCHAR(64) NOT NULL,
safety_level VARCHAR(64) DEFAULT 'AAA级合规认证',
status VARCHAR(32) DEFAULT '正常营业',
approved TINYINT DEFAULT 1
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 9. 风控拦截日志表
await conn.query(`
CREATE TABLE IF NOT EXISTS safety_audit_logs (
id VARCHAR(64) PRIMARY KEY,
timestamp_str VARCHAR(64) NOT NULL,
user_id INT NOT NULL,
type VARCHAR(64) NOT NULL,
content TEXT NOT NULL,
action TEXT NOT NULL,
status VARCHAR(32) DEFAULT '已处置',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 10. 财务抽成与分销配置表
await conn.query(`
CREATE TABLE IF NOT EXISTS finance_config (
id INT PRIMARY KEY DEFAULT 1,
platform_rate_percent DECIMAL(5,2) DEFAULT 10.0,
first_referral_percent DECIMAL(5,2) DEFAULT 5.0,
second_referral_percent DECIMAL(5,2) DEFAULT 2.0,
min_withdraw_amount INT DEFAULT 50,
max_withdraw_amount INT DEFAULT 20000,
withdraw_fee_percent DECIMAL(5,2) DEFAULT 0.0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 11. 全平台注册用户表 (普通玩家/认证大神/房主)
await conn.query(`
CREATE TABLE IF NOT EXISTS users (
id INT PRIMARY KEY AUTO_INCREMENT,
user_no VARCHAR(32) UNIQUE NOT NULL,
nickname VARCHAR(64) NOT NULL,
avatar TEXT,
phone VARCHAR(32) NOT NULL,
gender VARCHAR(10) DEFAULT '保密',
role VARCHAR(32) NOT NULL DEFAULT '普通玩家',
balance DECIMAL(10,2) DEFAULT 0.00,
order_count INT DEFAULT 0,
activity_count INT DEFAULT 0,
status VARCHAR(32) DEFAULT '正常',
wechat_openid VARCHAR(64),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 12. 友圈动态表
await conn.query(`
CREATE TABLE IF NOT EXISTS posts (
id VARCHAR(64) PRIMARY KEY,
user_id INT NOT NULL DEFAULT 1001,
author VARCHAR(64) NOT NULL,
avatar TEXT,
tag VARCHAR(64) DEFAULT '同城搭子',
tag_type VARCHAR(32) DEFAULT 'warm-tag',
location VARCHAR(255) DEFAULT '北京市公开商圈',
distance VARCHAR(64) DEFAULT '100m内',
content TEXT NOT NULL,
topics JSON,
images JSON,
action_text VARCHAR(32) DEFAULT '加入车队',
likes INT DEFAULT 0,
comments INT DEFAULT 0,
views INT DEFAULT 1,
category VARCHAR(32) NOT NULL DEFAULT 'partner',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 13. 订单真实评价与评分表
await conn.query(`
CREATE TABLE IF NOT EXISTS order_reviews (
id VARCHAR(64) PRIMARY KEY,
order_id VARCHAR(64) NOT NULL,
expert_id INT NOT NULL,
user_id INT NOT NULL DEFAULT 1001,
user_nickname VARCHAR(64) NOT NULL DEFAULT '买家用户',
user_avatar TEXT,
rating INT NOT NULL DEFAULT 5,
tags JSON,
content TEXT,
reply_content TEXT,
reply_time DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_order (order_id),
INDEX idx_expert (expert_id),
UNIQUE KEY uq_order_review (order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 历史库补丁:同一订单仅允许一条评价,防止重复评价反复触发结算
try {
await conn.query('ALTER TABLE order_reviews ADD UNIQUE KEY uq_order_review (order_id)');
} catch (_) {
// 约束已存在或历史数据存在重复,忽略(应用层仍会二次校验)
}
try {
await conn.query('ALTER TABLE order_reviews ADD COLUMN reply_content TEXT AFTER content');
} catch (_) {}
try {
await conn.query('ALTER TABLE order_reviews ADD COLUMN reply_time DATETIME AFTER reply_content');
} catch (_) {}
// 14. 友圈动态互动评论表
await conn.query(`
CREATE TABLE IF NOT EXISTS post_comments (
id VARCHAR(64) PRIMARY KEY,
post_id VARCHAR(64) NOT NULL,
user_id INT NOT NULL DEFAULT 1001,
author VARCHAR(64) NOT NULL DEFAULT '热心搭子',
avatar TEXT,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_post (post_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 15. 友圈搭子互动约伴上车申请表
await conn.query(`
CREATE TABLE IF NOT EXISTS post_applies (
id VARCHAR(64) PRIMARY KEY,
post_id VARCHAR(64) NOT NULL,
applicant_id INT NOT NULL DEFAULT 1001,
applicant_name VARCHAR(64) NOT NULL DEFAULT '搭子玩家',
applicant_avatar TEXT,
status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_post_applies (post_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 16. 友圈动态用户点赞明细表 (用于持久化用户点赞状态并防重)
await conn.query(`
CREATE TABLE IF NOT EXISTS post_likes (
id VARCHAR(64) PRIMARY KEY,
post_id VARCHAR(64) NOT NULL,
user_id INT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_post_user (post_id, user_id),
INDEX idx_post_likes_user (user_id),
INDEX idx_post_likes_post (post_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 17. 首页轮播图运营配置表
await conn.query(`
CREATE TABLE IF NOT EXISTS banners (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(128) NOT NULL,
subtitle VARCHAR(255) DEFAULT '',
tag VARCHAR(64) DEFAULT '热门活动',
badge VARCHAR(64) DEFAULT 'HOT 热门组局',
badge_type VARCHAR(64) DEFAULT 'badge-hot',
bg VARCHAR(255) DEFAULT 'linear-gradient(135deg, #F43F5E 0%, #FB7185 100%)',
image TEXT,
link VARCHAR(255) DEFAULT '/pages/activity/list',
sort_order INT DEFAULT 0,
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_sort (sort_order),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 17. 首页金刚区活动品类导航配置表
await conn.query(`
CREATE TABLE IF NOT EXISTS nav_categories (
id INT PRIMARY KEY AUTO_INCREMENT,
code VARCHAR(64) UNIQUE NOT NULL,
name VARCHAR(64) NOT NULL,
icon TEXT NOT NULL,
bg_class VARCHAR(64) DEFAULT 'bg-indigo',
cat VARCHAR(64) DEFAULT 'all',
target_url VARCHAR(255) NOT NULL DEFAULT '/pages/activity/list?category=ALL',
badge_text VARCHAR(32) DEFAULT '',
sort_order INT DEFAULT 0,
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_sort (sort_order),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 18. 钱包充值阶梯与赠送配置表
await conn.query(`
CREATE TABLE IF NOT EXISTS recharge_tiers (
id INT PRIMARY KEY AUTO_INCREMENT,
amount DECIMAL(10,2) NOT NULL,
bonus_amount DECIMAL(10,2) DEFAULT 0.00,
tag VARCHAR(32) DEFAULT '',
sort_order INT DEFAULT 0,
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_sort (sort_order),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 19. 平台营销优惠券配置与发放表
await conn.query(`
CREATE TABLE IF NOT EXISTS coupons (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(64) NOT NULL,
discount_amount DECIMAL(10,2) NOT NULL,
min_spend DECIMAL(10,2) NOT NULL DEFAULT 0.00,
valid_days INT NOT NULL DEFAULT 7,
total_count INT NOT NULL DEFAULT 1000,
received_count INT NOT NULL DEFAULT 0,
category_limit VARCHAR(64) DEFAULT 'ALL',
description VARCHAR(255) DEFAULT '',
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 20. 平台官方客服、公告及服务协议通用配置表
await conn.query(`
CREATE TABLE IF NOT EXISTS platform_configs (
id INT PRIMARY KEY AUTO_INCREMENT,
config_key VARCHAR(64) UNIQUE NOT NULL,
name VARCHAR(128) NOT NULL,
value_json JSON NOT NULL,
description VARCHAR(255) DEFAULT '',
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_key (config_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 初始化配置单条记录
const [cfgRows]: any = await conn.query('SELECT COUNT(*) as count FROM finance_config WHERE id = 1');
if (cfgRows[0].count === 0) {
await conn.query(`
INSERT INTO finance_config (id, platform_rate_percent, first_referral_percent, second_referral_percent, min_withdraw_amount, max_withdraw_amount, withdraw_fee_percent)
VALUES (1, 10.0, 5.0, 2.0, 50, 20000, 0.0);
`);
}
// 扩展 users 表结构: commission, invited_count
try {
await conn.query('ALTER TABLE users ADD COLUMN commission DECIMAL(10,2) DEFAULT 0.00');
} catch (_) {}
try {
await conn.query('ALTER TABLE users ADD COLUMN invited_count INT DEFAULT 0');
} catch (_) {}
try {
await conn.query('UPDATE users SET commission = 128.50, invited_count = 8 WHERE id = 1001 AND (commission IS NULL OR commission = 0)');
} catch (_) {}
// 扩展 users 表结构: 个性签名与大神认证标识
try {
await conn.query("ALTER TABLE users ADD COLUMN signature VARCHAR(64) DEFAULT ''");
} catch (_) {}
try {
await conn.query('ALTER TABLE users ADD COLUMN is_expert TINYINT DEFAULT 0');
} catch (_) {}
try {
await conn.query('ALTER TABLE users ADD COLUMN role_flags TINYINT DEFAULT 1');
await conn.query('UPDATE users SET role_flags = 3 WHERE is_expert = 1 OR role = "认证大神"');
} catch (_) {}
// 确保存在 demo 账号 101 (金牌陪伴师/认证大神体验账号)
try {
await conn.query(`
INSERT INTO users (id, user_no, nickname, avatar, phone, gender, role, balance, commission, invited_count, order_count, activity_count, status, wechat_openid, signature, is_expert, role_flags)
VALUES (101, 'U101', '糖心小糯米', 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200', '138****0101', '女', '认证大神', 2580.00, 128.50, 6, 88, 16, '正常', 'wx_openid_101', '人美声甜,擅长各类同城陪伴与开黑互动', 1, 3)
ON DUPLICATE KEY UPDATE nickname = VALUES(nickname), is_expert = 1, role_flags = 3
`);
} catch (_) {}
// 扩展 posts 表结构: is_top (置顶), status (正常/隐藏)
try {
await conn.query('ALTER TABLE posts ADD COLUMN is_top TINYINT DEFAULT 0');
} catch (_) {}
try {
await conn.query("ALTER TABLE posts ADD COLUMN status VARCHAR(32) DEFAULT '正常'");
} catch (_) {}
// 21. 用户举报工单表 (涉黄/涉赌/线下欺诈等不良陪玩举报)
await conn.query(`
CREATE TABLE IF NOT EXISTS reports (
id VARCHAR(64) PRIMARY KEY,
reporter_id INT NOT NULL,
target_type VARCHAR(32) NOT NULL DEFAULT 'EXPERT',
target_id VARCHAR(64) NOT NULL,
target_name VARCHAR(64) DEFAULT '',
reason VARCHAR(64) NOT NULL,
description VARCHAR(500) DEFAULT '',
evidence JSON,
status VARCHAR(32) NOT NULL DEFAULT '待处理',
handle_result VARCHAR(64) DEFAULT '',
handle_note VARCHAR(500) DEFAULT '',
handled_at DATETIME DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_target (target_type, target_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
try {
await conn.query('ALTER TABLE reports ADD COLUMN handle_result VARCHAR(64) DEFAULT ""');
} catch (_) {}
try {
await conn.query('ALTER TABLE reports ADD COLUMN handle_note VARCHAR(500) DEFAULT ""');
} catch (_) {}
try {
await conn.query('ALTER TABLE reports ADD COLUMN handled_at DATETIME DEFAULT NULL');
} catch (_) {}
// 22. 陪玩收藏表 (收藏喜欢的陪玩,实时高亮在接单状态)
await conn.query(`
CREATE TABLE IF NOT EXISTS favorites (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
expert_id INT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_expert (user_id, expert_id),
INDEX idx_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 23. 分销体系扩展 (父子终身绑定关系表、下级团队与佣金订单记录)
try {
await conn.query('ALTER TABLE users ADD COLUMN inviter_id INT DEFAULT NULL');
} catch (_) {}
await conn.query(`
CREATE TABLE IF NOT EXISTS distribution_relations (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT UNIQUE NOT NULL,
inviter_id INT NOT NULL,
level2_inviter_id INT DEFAULT NULL,
bind_source VARCHAR(64) DEFAULT 'INVITE_CODE',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_inviter (inviter_id),
INDEX idx_level2_inviter (level2_inviter_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
await conn.query(`
CREATE TABLE IF NOT EXISTS distribution_records (
id VARCHAR(64) PRIMARY KEY,
user_id INT NOT NULL,
from_user_id INT NOT NULL,
from_user_name VARCHAR(64) NOT NULL,
order_no VARCHAR(64) NOT NULL,
order_amount DECIMAL(10,2) NOT NULL,
commission DECIMAL(10,2) NOT NULL,
level INT NOT NULL DEFAULT 1,
rate_percent DECIMAL(5,2) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT '已入账',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_dist_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 站内消息通知中心:订单/提现/报名/退款/仲裁等关键动作落库,供小程序「消息中心」查看
await conn.query(`
CREATE TABLE IF NOT EXISTS user_notifications (
id VARCHAR(64) PRIMARY KEY,
user_id INT NOT NULL,
type VARCHAR(32) NOT NULL DEFAULT 'SYSTEM',
title VARCHAR(128) NOT NULL,
content VARCHAR(512) NOT NULL DEFAULT '',
link VARCHAR(255) NOT NULL DEFAULT '',
is_read TINYINT NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_notif_user_read (user_id, is_read, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 系统告警与后台操作审计表
await conn.query(`
CREATE TABLE IF NOT EXISTS system_alerts (
id VARCHAR(64) PRIMARY KEY,
alert_key VARCHAR(160) NOT NULL,
level VARCHAR(16) NOT NULL DEFAULT 'WARN',
title VARCHAR(255) NOT NULL,
detail TEXT,
status VARCHAR(16) NOT NULL DEFAULT 'OPEN',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
resolved_at DATETIME NULL,
UNIQUE KEY uq_alert_key (alert_key),
INDEX idx_alert_status (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
await conn.query(`
CREATE TABLE IF NOT EXISTS admin_audit_logs (
id VARCHAR(64) PRIMARY KEY,
operator VARCHAR(64) NOT NULL DEFAULT 'admin',
action VARCHAR(64) NOT NULL,
target_type VARCHAR(32) DEFAULT '',
target_id VARCHAR(64) DEFAULT '',
detail TEXT,
ip VARCHAR(64) DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_audit_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 管理端消息铃铛:待办已读状态(记录已读时数量,新增待办会重新变未读)
await conn.query(`
CREATE TABLE IF NOT EXISTS admin_message_reads (
todo_key VARCHAR(64) PRIMARY KEY,
read_count INT NOT NULL DEFAULT 0,
read_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 用户已领取的优惠券(券中心领取 → 下单核销 → 取消/退款回退)
await conn.query(`
CREATE TABLE IF NOT EXISTS user_coupons (
id VARCHAR(64) PRIMARY KEY,
user_id INT NOT NULL,
coupon_id INT NOT NULL,
name VARCHAR(64) NOT NULL DEFAULT '',
discount_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
min_spend DECIMAL(10,2) NOT NULL DEFAULT 0.00,
category_limit VARCHAR(64) DEFAULT 'ALL',
status VARCHAR(16) NOT NULL DEFAULT 'UNUSED',
order_id VARCHAR(64) NULL,
claimed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expired_at DATETIME NULL,
used_at DATETIME NULL,
INDEX idx_uc_user_status (user_id, status),
INDEX idx_uc_coupon_user (coupon_id, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 一次性清理:历史版本会自动为大神生成 3 个演示服务包(现已改为大神自助维护),删除演示数据
try {
await conn.query(`
DELETE FROM expert_services
WHERE title IN ('排位保星连胜局', '娱乐匹配快乐唠嗑局', '英雄技术单项教学指导')
`);
} catch (_) {
// expert_services 尚未创建:忽略
}
// 慢查询优化:为高频查询列补充索引(幂等:已存在则忽略;表不存在也忽略)
// 注意:必须在所有建表语句之后执行,确保目标表已存在。
const indexDefs: Array<[string, string, string]> = [
['orders', 'idx_orders_buyer', 'buyer_id'],
['orders', 'idx_orders_expert', 'expert_id'],
['orders', 'idx_orders_claimed_by', 'claimed_by'],
['orders', 'idx_orders_status_created', 'status, created_at'],
['orders', 'idx_orders_verify_code', 'verify_code'],
['orders', 'idx_orders_companion_name', 'companion_name'],
['order_reviews', 'idx_reviews_expert_created', 'expert_id, created_at'],
['finance_records', 'idx_fin_user', 'user_id'],
['finance_records', 'idx_fin_related', 'related_id'],
['finance_records', 'idx_fin_type', 'type'],
['finance_records', 'idx_fin_created', 'created_at'],
['favorites', 'idx_fav_user', 'user_id'],
['favorites', 'idx_fav_expert', 'expert_id'],
['distribution_records', 'idx_dist_created', 'created_at'],
['posts', 'idx_posts_status_top_created', 'status, is_top, created_at'],
['posts', 'idx_posts_category', 'category'],
['reports', 'idx_reports_status', 'status'],
['reports', 'idx_reports_reporter', 'reporter_id'],
['withdrawals', 'idx_wd_companion', 'companion_id'],
['withdrawals', 'idx_wd_status', 'status'],
['activities', 'idx_act_creator', 'creator_id'],
['activities', 'idx_act_status', 'status'],
['activities', 'idx_act_status_created', 'status, created_at'],
['withdrawals', 'idx_wd_status_created', 'status, created_at'],
['payment_intents', 'idx_pi_status_created', 'status, created_at'],
['payment_intents', 'idx_pi_order', 'order_id'],
['payment_intents', 'idx_pi_status', 'status'],
['users', 'idx_users_phone', 'phone'],
['users', 'idx_users_inviter', 'inviter_id'],
['users', 'idx_users_nickname', 'nickname'],
['participants', 'idx_pt_user', 'user_id'],
['post_likes', 'idx_post_likes_post', 'post_id'],
['post_comments', 'idx_pc_post', 'post_id'],
['post_applies', 'idx_pa_post', 'post_id'],
['post_applies', 'idx_pa_status', 'status'],
['arbitrations', 'idx_arb_status', 'status'],
['arbitrations', 'idx_arb_order', 'order_id'],
['companions', 'idx_comp_user', 'user_id'],
['companions', 'idx_comp_status', 'status'],
['companions', 'idx_comp_active', 'status, last_active_at'],
['companions', 'idx_comp_audit', 'audit_status'],
['payment_refunds', 'idx_pr_order', 'order_id'],
];
for (const [table, name, cols] of indexDefs) {
try {
await conn.query(`ALTER TABLE \`${table}\` ADD INDEX \`${name}\` (${cols})`);
} catch (_) {
// 索引已存在或表不存在:忽略
}
}
// 数据修复:历史抢单订单的 expert_id 曾被写成用户 id,统一回填为大神档案 id
// (幂等:回填后 expert_id=c.id,通常不再等于 claimed_by)
try {
await conn.query(`
UPDATE orders o
JOIN companions c ON c.user_id = o.expert_id
SET o.expert_id = c.id
WHERE o.claimed_by > 0 AND o.expert_id = o.claimed_by
`);
} catch (_) {
// 表不存在或数据不满足条件:忽略
}
// 检查并插入初始种子数据(若表为空)
await seedInitialDataIfEmpty(conn);
console.log('[MySQL] peiwan 数据库核心表及种子数据已完整就绪');
} finally {
conn.release();
}
}
async function seedInitialDataIfEmpty(conn: any): Promise<void> {
// 1. 种子活动
const [actRows]: any = await conn.query('SELECT COUNT(*) as count FROM activities');
if (actRows[0].count === 0) {
await conn.query(`
INSERT INTO activities (id, creator_id, title, game_category, location, pay_type, total_fee_cents, per_person_fee_cents, max_participants, allow_dynamic_price, price_adjust_rule)
VALUES
('act_101', 1001, '三里屯铜锅涮肉AA探店 · 8人热烈局', '同城陪吃', '北京市朝阳区三里屯聚宝源铜锅涮肉', 1, 40000, 5000, 4, 1, 'UNIFIED_HIGH'),
('act_102', 1002, '工体纯KTV沉浸式麦霸欢唱 · 寻找甜美副唱', '同城陪唱', '工体西路8号纯KTV 888包厢', 2, 240000, 80000, 3, 1, 'UNIFIED_HIGH'),
('act_103', 1003, '望京合生麒麟社微醺清吧精酿品鉴', '同城陪喝', '望京街道合生麒麟社地库跳海酒馆', 1, 39600, 19800, 4, 1, 'UNIFIED_HIGH');
`);
// 种子参与者与4位核销码
await conn.query(`
INSERT INTO participants (id, activity_id, user_id, pay_amount_cents, verify_code, checkin_status, verified_at, verified_by)
VALUES
('pt_101', 'act_101', 2001, 5000, '8392', 1, NOW(), 1001),
('pt_102', 'act_101', 2002, 5000, '5419', 0, NULL, NULL),
('pt_103', 'act_101', 2003, 5000, '7260', 0, NULL, NULL),
('pt_104', 'act_102', 2004, 80000, '9031', 0, NULL, NULL),
('pt_105', 'act_103', 2005, 19800, '6184', 0, NULL, NULL);
`);
}
// 2. 种子大神档案
const [compRows]: any = await conn.query('SELECT COUNT(*) as count FROM companions');
if (compRows[0].count === 0) {
await conn.query(`
INSERT INTO companions (name, avatar, gender, age, track, rank_name, hourly_rate, voice_duration, voice_title, rating, order_count, status, audit_status, id_card_masked)
VALUES
('苏苏学姐', 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120', '女', 22, '同城陪吃', '金牌探店官', 158, 18, '甜美学姐元气语音条', 4.95, 382, '接单中', '已通过', '110105********3821'),
('林深见鹿', 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=120', '男', 24, 'KTV陪唱', '十佳青年歌手', 288, 25, '清澈烟嗓流行情歌试听', 4.98, 512, '接单中', '已通过', '310115********1028'),
('小鱼泡泡', 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=120', '女', 21, '同城陪喝', '精酿侍酒达人', 198, 14, '微醺甜美随和聊天', 4.88, 194, '休息中', '已通过', '440301********7824'),
('顾北辰', 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=120', '男', 23, '线上电竞', '百星王者国服第一射手', 88, 12, '低沉磁性指挥型男音', 4.92, 680, '接单中', '已通过', '510107********4419'),
('可可奶芙', 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=120', '女', 20, '同城陪吃', '美食品鉴官', 168, 16, '软糯元气自我介绍', 4.85, 42, '待审核', '待审核', '320106********9124'),
('夜风沉醉', 'https://images.unsplash.com/photo-1522075469751-3a6694fb2f61?w=120', '男', 25, 'KTV陪唱', '摇滚乐队副主唱', 220, 22, '摇滚副歌现场高音试听', 4.75, 18, '待审核', '待审核', '330104********2816');
`);
}
// 3. 种子订单
const [ordRows]: any = await conn.query('SELECT COUNT(*) as count FROM orders');
if (ordRows[0].count === 0) {
await conn.query(`
INSERT INTO orders (id, order_no, client_name, companion_name, track, expense_burden, hourly_rate, duration_hours, amount, location, status)
VALUES
('ord_01', 'PW202609280001', '赵公子', '苏苏学姐', '同城陪吃', '客户全包', 158, 2, 316.00, '朝阳区大悦城 太二酸菜鱼', '已到场核销'),
('ord_02', 'PW202609280002', '钱老板', '林深见鹿', 'KTV陪唱', '客户全包', 288, 3, 864.00, '工体西路8号纯KTV 888包厢', '履约中'),
('ord_03', 'PW202609280003', '孙先生', '小鱼泡泡', '同城陪喝', '费用AA', 198, 2, 396.00, '望京合生麒麟社跳海酒馆', '待支付'),
('ord_04', 'PW202609280004', '李小姐', '顾北辰', '线上电竞', '无需承担', 88, 4, 352.00, '线上语音频道 #9021', '已完成'),
('ord_05', 'PW202609280005', '周先生', '林深见鹿', 'KTV陪唱', '客户全包', 288, 2, 576.00, '海淀中关村好乐迪KTV', '争议仲裁中'),
('ord_06', 'PW202609280006', '吴老板', '待接单大神', '同城陪伴', '客户全包', 128, 2, 256.00, '朝阳区三里屯商圈', '待接单');
`);
}
// 4. 种子仲裁
const [arbRows]: any = await conn.query('SELECT COUNT(*) as count FROM arbitrations');
if (arbRows[0].count === 0) {
await conn.query(`
INSERT INTO arbitrations (id, order_id, order_no, client_name, companion_name, reason, user_evidence, status)
VALUES
('arb_001', 'ord_05', 'PW202609280005', '周先生', '林深见鹿', '未准时到场且提前离场', '大神迟到了35分钟,且中途以有急事为由提前40分钟离开,实际服务仅45分钟。现场有微信沟通记录与到场照片。', '待平台仲裁');
`);
}
// 5. 种子财务流水
const [finRows]: any = await conn.query('SELECT COUNT(*) as count FROM finance_records');
if (finRows[0].count === 0) {
await conn.query(`
INSERT INTO finance_records (id, record_no, type, amount, direction, counterparty, related_id)
VALUES
('fin_001', 'TR20260928001', '活动报名入账', 50.00, 'IN', '用户 2001', 'act_101'),
('fin_002', 'TR20260928002', '活动报名入账', 50.00, 'IN', '用户 2002', 'act_101'),
('fin_003', 'TR20260928003', '大神订单全款', 316.00, 'IN', '客户 赵公子', 'ord_01'),
('fin_004', 'TR20260928004', '平台抽成 (10%)', 31.60, 'IN', '系统结算', 'ord_01'),
('fin_005', 'TR20260928005', '一级分销佣金 (5%)', 15.80, 'OUT', '推荐人 张三', 'ord_01'),
('fin_006', 'TR20260928006', '房主追加赏金差额退补', 300.00, 'IN', '房主 1001', 'act_101');
`);
}
// 6. 种子提现
const [wdRows]: any = await conn.query('SELECT COUNT(*) as count FROM withdrawals');
if (wdRows[0].count === 0) {
await conn.query(`
INSERT INTO withdrawals (id, withdraw_no, companion_name, companion_id, amount, balance, channel, status)
VALUES
('wd_001', 'TX20260928001', '苏苏学姐', 101, 1250.00, 4890.00, '微信商户零钱', '待审核'),
('wd_002', 'TX20260928002', '顾北辰', 104, 2800.00, 6200.00, '银联企业直连', '已打款');
`);
}
// 7. 种子场所
const [placeRows]: any = await conn.query('SELECT COUNT(*) as count FROM safety_places');
if (placeRows[0].count === 0) {
await conn.query(`
INSERT INTO safety_places (id, name, area, category, safety_level, status, approved)
VALUES
('place_001', '三里屯聚宝源铜锅涮肉', '朝阳区三里屯商圈', '餐饮美食', 'AAA级合规认证', '正常营业', 1),
('place_002', '工体纯KTV', '朝阳区工人体育场商圈', '量贩KTV', 'AAA级合规认证', '正常营业', 1),
('place_003', '望京合生麒麟社跳海酒馆', '朝阳区望京商圈', '商圈清吧', 'AA级合规认证', '正常营业', 1),
('place_004', '朝阳大悦城太二酸菜鱼', '朝阳区青年路大悦城', '餐饮美食', 'AAA级合规认证', '正常营业', 1);
`);
}
// 8. 种子风控拦截日志
const [secRows]: any = await conn.query('SELECT COUNT(*) as count FROM safety_audit_logs');
if (secRows[0].count === 0) {
await conn.query(`
INSERT INTO safety_audit_logs (id, timestamp_str, user_id, type, content, action, status)
VALUES
('sec_001', '2026-09-28 14:12:05', 2088, '违规场所拦截', '用户尝试将线下陪吃地点填写为:某某苑4号楼2单元501室(私人住宅)', '系统自动阻断下单,弹出公共商圈合规红线告警', '已处置'),
('sec_002', '2026-09-28 11:35:20', 2095, '违规场所拦截', '用户尝试将线下陪唱地点填写为:某某精品快捷酒店308客房', '系统拦截并强制要求修改为正规商业量贩KTV', '已处置'),
('sec_003', '2026-09-27 21:04:18', 2012, '敏感词过滤', '组局留言中出现敏感违规引流词汇汇款、加私微等', '关键词自动打码屏蔽,并记录账号风控提示分', '已处置');
`);
}
// 9. 种子全平台注册用户
const [userRows]: any = await conn.query('SELECT COUNT(*) as count FROM users');
if (userRows[0].count === 0) {
await conn.query(`
INSERT INTO users (id, user_no, nickname, avatar, phone, gender, role, balance, order_count, activity_count, status, wechat_openid, created_at)
VALUES
(1001, 'U1001', '同城局长', 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=120', '138****1001', '男', '同城房主', 1880.00, 8, 12, '正常', 'wx_openid_1001', '2026-09-01 10:00:00'),
(2001, 'U2001', '涮肉小行家', 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120', '139****2001', '女', '普通玩家', 260.00, 15, 6, '正常', 'wx_openid_2001', '2026-09-05 14:20:00'),
(2002, 'U2002', '吃货小张', 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=120', '136****2002', '男', '普通玩家', 120.00, 6, 2, '正常', 'wx_openid_2002', '2026-09-10 11:15:00'),
(2003, 'U2003', 'K歌达人阿伟', 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=120', '137****2003', '男', '普通玩家', 350.00, 21, 9, '正常', 'wx_openid_2003', '2026-09-12 18:30:00'),
(2004, 'U2004', '电竞刺客', 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=120', '135****2004', '男', '普通玩家', 50.00, 9, 3, '正常', 'wx_openid_2004', '2026-09-15 20:00:00'),
(2005, 'U2005', '橙子小七', 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120', '138****2005', '女', '认证大神', 4280.00, 420, 28, '正常', 'wx_openid_2005', '2026-09-02 09:00:00'),
(2006, 'U2006', '苏苏学姐', 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=120', '139****2006', '女', '认证大神', 5690.00, 560, 35, '正常', 'wx_openid_2006', '2026-09-03 16:45:00'),
(2007, 'U2007', '莫吉托学长', 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=120', '137****2007', '男', '认证大神', 3100.00, 310, 18, '正常', 'wx_openid_2007', '2026-09-08 12:20:00'),
(2008, 'U2008', '测试玩家小李', 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=120', '188****2008', '男', '普通玩家', 88.00, 2, 1, '正常', 'wx_openid_2008', '2026-09-28 10:10:00'),
(2009, 'U2009', '风险异常号', 'https://images.unsplash.com/photo-1492562080023-ab3db95bfbce?w=120', '131****2009', '未知', '普通玩家', 0.00, 0, 0, '已封禁', 'wx_openid_2009', '2026-09-27 15:30:00');
`);
}
// 10. 种子友圈动态
const [postRows]: any = await conn.query('SELECT COUNT(*) as count FROM posts');
if (postRows[0].count === 0) {
await conn.query(`
INSERT INTO posts (id, user_id, author, avatar, tag, tag_type, location, distance, content, topics, images, action_text, likes, comments, views, category, created_at)
VALUES
('post_1', 2005, '小甜橙', 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150', '火锅达人', 'warm-tag', '朝阳区 · 三里屯太古里', '800m', '今晚7点半有没有在三里屯附近的小伙伴一起去吃铜锅涮肉?已预订包间,目前已有2人,再来2个饭搭子AA!吃完还可以顺路去清吧听歌放松~', '["三里屯探店", "铜锅涮肉AA", "饭搭子招募"]', '["https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=500", "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=500"]', '加入车队', 42, 8, 310, 'partner', NOW() - INTERVAL 15 MINUTE),
('post_2', 2004, '暗影刺客 (省一李白)', 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150', '国服大神', 'cool-tag', '海淀区 · 五道口', '3.5km', '今晚王者荣耀荣耀王者冲分车队!目前缺一个听指挥的中单或者软辅妹子,全程开麦包赢不保段包赔,想上星的速速滴我!', '["王者荣耀", "荣耀车队", "连胜冲分"]', '["https://images.unsplash.com/photo-1542751371-adc38448a05e?w=500"]', '立即上车', 76, 15, 680, 'game', NOW() - INTERVAL 40 MINUTE),
('post_3', 2006, '苏苏学姐', 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=150', '金牌麦霸', 'gold-tag', '朝阳区 · 工体商业街', '1.2km', '工体纯K量贩欢唱局!甜美情歌、粤语老歌、摇滚气氛拉满。现场备有精美果盘与特饮,今晚欢唱不嗨不散,还有专属歌声试听哦!', '["KTV欢唱", "量贩陪唱", "同城聚会"]', '["https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?w=500"]', '报名同去', 58, 11, 520, 'partner', NOW() - INTERVAL 1 HOUR);
`);
}
// 11. 种子订单真实评价
const [revRows]: any = await conn.query('SELECT COUNT(*) as count FROM order_reviews');
if (revRows[0].count === 0) {
await conn.query(`
INSERT INTO order_reviews (id, order_id, expert_id, user_id, user_nickname, user_avatar, rating, tags, content, created_at)
VALUES
('rev_101', 'ord_04', 104, 1001, '李小姐', 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120', 5, '["技术过硬", "耐心温柔", "连胜吃鸡"]', '北辰大神意识太好了,几波团战残血反杀,全程听指挥轻轻松松上大分!', NOW() - INTERVAL 1 DAY),
('rev_102', 'ord_01', 101, 2001, '赵公子', 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=120', 5, '["随和健谈", "品味极佳", "准时守约"]', '苏苏学姐推荐的涮肉店真的一绝,性格温柔幽默,聚餐氛围特别好!', NOW() - INTERVAL 2 DAY);
`);
}
// 12. 种子友圈动态评论
const [cmtRows]: any = await conn.query('SELECT COUNT(*) as count FROM post_comments');
if (cmtRows[0].count === 0) {
await conn.query(`
INSERT INTO post_comments (id, post_id, user_id, author, avatar, content, created_at)
VALUES
('cmt_1', 'post_1', 2002, '吃货小张', 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=120', '三里屯聚宝源吗?这家手切羊肉超棒,已报名+1!', NOW() - INTERVAL 10 MINUTE),
('cmt_2', 'post_1', 2006, '苏苏学姐', 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=120', '吃完可以去附近跳海酒馆坐坐,环境很舒服~', NOW() - INTERVAL 5 MINUTE),
('cmt_3', 'post_2', 2001, '涮肉小行家', 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120', '国服李白太强了!主玩大乔和张飞,晚上可以一起开黑吗?', NOW() - INTERVAL 25 MINUTE);
`);
}
// 13. 种子轮播图
const [bnrRows]: any = await conn.query('SELECT COUNT(*) as count FROM banners');
if (bnrRows[0].count === 0) {
await conn.query(`
INSERT INTO banners (id, title, subtitle, tag, badge, badge_type, bg, image, link, sort_order, status)
VALUES
(1, '同城聚会狂欢 | 网红火锅探店与量贩KTV嗨唱', 'AA制铜锅涮肉 · 8人热烈局', '热门陪伴', 'HOT 热门组局', 'badge-hot', 'linear-gradient(135deg, #F43F5E 0%, #FB7185 100%)', 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=800', '/pages/activity/detail?id=act_101', 1, 1),
(2, '量贩KTV麦霸沉浸欢唱 | 音乐达人歌声试听', '工体纯K 888豪华包厢', 'K歌对唱', '同城陪唱', 'badge-ktv', 'linear-gradient(135deg, #7C3AED 0%, #6366F1 100%)', 'https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?w=800', '/pages/activity/detail?id=act_102', 2, 1),
(3, '精酿清吧微醺畅聊 | 线下陪吃陪喝立减30元', '望京跳海酒馆 · 慢调交友', '同城陪喝', '同城陪喝', 'badge-bar', 'linear-gradient(135deg, #059669 0%, #10B981 100%)', 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800', '/pages/activity/detail?id=act_103', 3, 1);
`);
}
// 14. 种子金刚区活动品类导航
const [navRows]: any = await conn.query('SELECT COUNT(*) as count FROM nav_categories');
if (navRows[0].count === 0) {
await conn.query(`
INSERT INTO nav_categories (id, code, name, icon, bg_class, cat, target_url, badge_text, sort_order, status)
VALUES
(1, 'esports', '电子竞技', 'gameAll', 'bg-indigo', 'all', '/pages/activity/list?category=ALL', 'HOT', 1, 1),
(2, 'meal', '美食探店', 'utensils', 'bg-amber', 'offline_meal', '/pages/activity/list?category=MEAL', 'AA制', 2, 1),
(3, 'sing', 'KTV嗨唱', 'micVocal', 'bg-rose', 'offline_sing', '/pages/activity/list?category=SING', '热度高', 3, 1),
(4, 'billiards', '同城台球', 'billiards', 'bg-sky', 'offline_play', '/pages/activity/list?category=ALL', '', 4, 1),
(5, 'hiking', '户外爬山', 'mountain', 'bg-emerald', 'offline_play', '/pages/activity/list?category=ALL', '周末', 5, 1),
(6, 'drink', '下午茶歇', 'coffee', 'bg-teal', 'offline_drink', '/pages/activity/list?category=DRINK', '', 6, 1),
(7, 'boardgame', '剧本桌游', 'dice', 'bg-purple', 'offline_play', '/pages/activity/list?category=ALL', '', 7, 1),
(8, 'fitness', '运动健身', 'fitness', 'bg-orange', 'offline_play', '/pages/activity/list?category=ALL', '', 8, 1),
(9, 'bar', '同城微醺', 'warmFlame', 'bg-rose', 'offline_drink', '/pages/activity/list?category=DRINK', '清吧', 9, 1),
(10, 'more', '全部活动', 'gridAll', 'bg-blue', 'all', '/pages/activity/list?category=ALL', '', 10, 1);
`);
}
// 15. 种子充值阶梯
const [tierRows]: any = await conn.query('SELECT COUNT(*) as count FROM recharge_tiers');
if (tierRows[0].count === 0) {
await conn.query(`
INSERT INTO recharge_tiers (id, amount, bonus_amount, tag, sort_order, status)
VALUES
(1, 50.00, 0.00, '基础充值', 1, 1),
(2, 100.00, 5.00, '热门畅享', 2, 1),
(3, 200.00, 15.00, '超值加赠', 3, 1),
(4, 500.00, 50.00, '尊享特惠', 4, 1),
(5, 1000.00, 120.00, '土豪专享', 5, 1);
`);
}
// 16. 种子平台优惠券
const [cpnRows]: any = await conn.query('SELECT COUNT(*) as count FROM coupons');
if (cpnRows[0].count === 0) {
await conn.query(`
INSERT INTO coupons (id, name, discount_amount, min_spend, valid_days, total_count, received_count, category_limit, description, status)
VALUES
(1, '新用户注册专享立减券', 20.00, 50.00, 15, 2000, 128, 'ALL', '全品类陪玩及同城组局满50可用', 1),
(2, '周末同城聚会AA抵扣券', 15.00, 80.00, 7, 1000, 85, 'MEAL', '仅限同城美食探店组局活动使用', 1),
(3, '工体K歌麦霸欢唱减免券', 30.00, 120.00, 10, 800, 62, 'SING', '仅限KTV陪唱预约与活动使用', 1),
(4, '电竞开黑连胜冲星券', 10.00, 30.00, 30, 5000, 340, 'ALL', '线上游戏陪玩下单立减', 1);
`);
}
// 17. 种子平台通用配置(官方客服、履约公告指引、规范协议、话题、公告、FAQ)
const communityTopicsVal = JSON.stringify([
{ id: 'top_1', name: '三里屯探店', hot: true, postsCount: 1280 },
{ id: 'top_2', name: '铜锅涮肉AA', hot: true, postsCount: 890 },
{ id: 'top_3', name: '工体纯K麦霸', hot: false, postsCount: 650 },
{ id: 'top_4', name: '国服上分车队', hot: true, postsCount: 2310 },
{ id: 'top_5', name: '望京跳海微醺', hot: false, postsCount: 420 },
{ id: 'top_6', name: '同城搭子招募', hot: true, postsCount: 3100 }
]);
const communityNoticeVal = JSON.stringify({
title: '友圈同城互动与公约置顶',
content: '欢迎来到同城搭子友圈!文明交友,发布真实组局诉求;平台全天候对动态与评论实施微信官方内容安全过滤,严禁广告引流及私下交易。',
tag: '置顶推荐',
showInBanner: true
});
const faqListVal = JSON.stringify([
{
id: 'faq_1',
category: '新手预约与组局',
question: '什么是4位纯数字核销码?',
answer: '为确保搭子碰面和订单真实履约,下单成功或报名组局后系统自动生成专属4位防伪数字码,到达公开活动现场后出示给陪伴师或房主签到核销。'
},
{
id: 'faq_2',
category: '新手预约与组局',
question: 'AA制拼桌与房主请客有什么区别?',
answer: '【AA制拼桌】由参与人员平摊费用,报名时支付人均分摊金额;【房主请客/项目制】由发起房主全额买单,且房主可随时追加赏金或提高出价。'
},
{
id: 'faq_3',
category: '资金与退款',
question: '如何申请退款与争议仲裁?',
answer: '在订单【待接单】或【待付款】状态可随时取消全额退款;在【服务中】或遇违约可在订单详情点击【发起争议仲裁】,由平台官方运营人工介入判定。'
},
{
id: 'faq_4',
category: '大神与收益',
question: '成为陪玩大神有什么资质要求?',
answer: '需在小程序个人中心提交实名认证与技能资质,通过平台运营团队人工审核后,即可解锁接单工作台与附近雷达扫单功能。'
}
]);
const securityWordsVal = JSON.stringify([
'加微信私聊', '私下走微信', '走线下免佣金', '线下给现金', '私下单',
'联系方式v', 'vx号', '微信转账', '私加好友', '线下私约'
]);
const homePopupVal = JSON.stringify({
enabled: true,
title: '新人专享同城陪伴好礼',
subtitle: '注册即送20元无门槛立减券,首单专享特惠',
imageUrl: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=800',
link: '/pages/activity/list',
btnText: '立即领券组局',
frequency: 'once_daily' // once_daily | always | once_ever
});
const expertRulesVal = JSON.stringify({
minHourlyRate: 30,
maxHourlyRate: 500,
defaultHourlyRate: 88,
requireRealName: true,
allowCustomTags: true,
maxTagsCount: 6,
candidateTags: [
'国服王者', '声优达人', '幽默风趣', '随叫随到', '暖心倾听',
'百星野王', '金牌探店', '量贩麦霸', '微醺搭子', '绅士礼貌',
'活力开朗', '耐心教学', '高胜率车头', '拍照好看'
]
});
const financeRulesVal = JSON.stringify({
platformRatePercent: 10.0,
firstReferralPercent: 5.0,
secondReferralPercent: 2.0,
minWithdrawAmount: 50,
maxWithdrawAmount: 20000,
withdrawFeePercent: 0.0,
withdrawDesc: '提现将自动发起微信企业付款打款至微信零钱,工作日预计2小时内到账。'
});
const activityRulesVal = JSON.stringify({
minParticipants: 2,
maxParticipants: 10,
defaultParticipants: 4,
presetPerPersonFees: [200, 300, 500, 800, 1000],
presetTotalFees: [500, 1000, 2000, 3000, 5000],
defaultAaFee: 50
});
const messageTemplateRulesVal = JSON.stringify({
orderStatusTemplateId: 'tmpl_order_status_update_01',
activityJoinTemplateId: 'tmpl_activity_joined_02',
withdrawSuccessTemplateId: 'tmpl_withdraw_success_03',
verifyCodeTemplateId: 'tmpl_verify_code_notice_04',
enablePush: 1,
remark: '微信小程序订阅消息与服务通知模版ID配置'
});
const wechatPayConfigVal = JSON.stringify({
mode: 'AUTO',
appId: '',
appSecret: '',
mchId: '',
apiV3Key: '',
serialNo: '',
privateKey: '',
certPem: '',
platformPublicKeyId: '',
platformPublicKey: '',
notifyUrl: 'https://peiww.gynhc.com/api/pay/wechat/notify',
remark: '微信支付V3直连商户参数配置(配置完整后自动或手动切换至实盘JSAPI支付)'
});
const mapLocationConfigVal = JSON.stringify({
provider: 'tencent',
mapKey: '',
secretKey: '',
serviceName: '腾讯位置服务 (Tencent LBS)',
enableChooseLocation: 1,
enableDistanceCalculation: 1,
defaultCity: '北京市',
remark: '腾讯位置服务 Key(需在 lbs.qq.com 申请并勾选 WebServiceAPI 与微信小程序)'
});
const [cfgItemRows]: any = await conn.query('SELECT COUNT(*) as count FROM platform_configs');
if (cfgItemRows[0].count === 0) {
const customerServiceVal = JSON.stringify({
hotline: '400-888-6666',
hotlineDesc: '7x24h 官方客服服务热线',
wechat: 'peiwan_vip_kefu',
wechatDesc: '官方 VIP 客服微信号',
workHours: '周一至周日 00:00 - 24:00'
});
const safetyNoticeVal = JSON.stringify({
title: '平台官方公告与指引',
points: [
'【安全履约须知】为保障搭子与陪伴师人身与财产安全,线下约局须在合规公开商业街区进行。',
'【现场核销】现场碰面后请出示 4 位数字核销码进行签到确认。',
'【平台兜底】平台提供 7x24 小时争议仲裁与资金托管保障服务,严禁脱离平台私下交易。'
],
noticePopup: false
});
const protocolVal = JSON.stringify({
title: '用户服务协议与隐私规范',
summary: '严禁发布任何违规言论及低俗诈骗行为,平台全面遵循微信内容安全风控与用户隐私保护标准。',
userAgreementTitle: '用户服务协议',
userAgreementContent: '1. 账号使用规范:用户注册与使用同城生活陪伴服务须符合国家法律法规,严禁利用平台从事任何违法违规行为。\n2. 资金与交易保障:平台提供全额担保交易,订单完成前款项由官方账户托管,碰面出示4位核销码确认后自动结算至大神钱包。\n3. 履约安全守则:所有同城活动须在合规公开商业街区进行,严禁私下导流与脱离平台交易。\n4. 争议与售后仲裁:若遇服务不符或搭子违约,可随时在订单中发起申诉,由平台运营仲裁专员裁决。',
privacyPolicyTitle: '隐私保护指引',
privacyPolicyContent: '1. 信息收集范围:为保障同城陪玩交易安全与现场4位核销码核验,平台在获得您授权后会依法收集手机号与必要位置信息。\n2. 数据加密保护:平台全面接入微信小程序官方安全风控规范,所有用户数据均实施高强度加密脱敏存储。\n3. 权限撤回与管理:您可随时在小程序系统设置中撤回位置或通讯授权,平台未经您的明确许可绝不会向任何第三方泄露您的隐私数据。',
content: '1. 账号使用规范:用户注册与使用陪玩服务须符合国家法律法规,严禁利用平台从事违法违规行为。\n2. 资金与交易保障:平台提供全额担保交易,订单完成前款项由官方账户托管,核销确认后自动结算至大神钱包。\n3. 争议与售后:若遇服务不符或搭子违约,可随时提交争议仲裁由运营平台人工介入裁决。'
});
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)
VALUES
('customer_service', '官方在线客服与服务热线', ?, '小程序端个人中心客服联系方式', 1),
('safety_notice', '安全履约须知与官方公告', ?, '小程序端官方指引与安全红线弹窗', 1),
('platform_protocol', '平台服务协议与合规规范', ?, '小程序端服务协议与隐私政策详情', 1),
('community_topics', '友圈热门推荐话题库', ?, '友圈广场发帖推荐话题与热门标签', 1),
('community_notice', '友圈社区置顶头条公告', ?, '友圈广场顶部置顶横幅公告与社区公约', 1),
('faq_list', '帮助中心与常见问题FAQ', ?, '小程序端帮助中心常见问答与指引', 1),
('security_words', '私下引流与违规自定义敏感词库', ?, '全平台发帖、评论及订单防私下交易关键词实时过滤', 1),
('home_popup', '小程序首页运营全局弹窗', ?, '节日活动推广、新用户专享首单优惠全局浮层弹窗', 1),
('expert_rules', '大神技能标签库与定价区间规范', ?, '陪玩大神入驻审核、定价上下限限制与官方推荐技能标签库', 1),
('finance_rules', '平台抽成与推广分销规则', ?, '平台佣金抽成比例、二级分销返润与佣金提现门槛限制', 1),
('activity_rules', '同城组局发单与预算预设规范', ?, '招募席位上下限、快捷单人赏金预设与包场预算梯队', 1),
('message_template_rules', '微信服务通知与模板消息配置', ?, '微信订阅消息通知与状态模板配置', 1),
('wechat_pay_config', '微信支付商户号与证书配置', ?, '微信支付V3商户号、APIv3密钥与证书参数配置', 1),
('map_location_config', '地图定位与LBS服务配置', ?, '腾讯位置服务Key、选点与周边测距参数配置', 1);
`, [
customerServiceVal, safetyNoticeVal, protocolVal,
communityTopicsVal, communityNoticeVal, faqListVal,
securityWordsVal, homePopupVal, expertRulesVal,
financeRulesVal, activityRulesVal, messageTemplateRulesVal,
wechatPayConfigVal, mapLocationConfigVal
]);
} else {
// 增量保证各核心配置项完整就绪
const [existingRows]: any = await conn.query('SELECT config_key FROM platform_configs');
const existingKeys = new Set(existingRows.map((r: any) => r.config_key));
if (!existingKeys.has('community_topics')) {
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)
VALUES ('community_topics', '友圈热门推荐话题库', ?, '友圈广场发帖推荐话题与热门标签', 1)
`, [communityTopicsVal]);
}
if (!existingKeys.has('community_notice')) {
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)
VALUES ('community_notice', '友圈社区置顶头条公告', ?, '友圈广场顶部置顶横幅公告与社区公约', 1)
`, [communityNoticeVal]);
}
if (!existingKeys.has('faq_list')) {
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)
VALUES ('faq_list', '帮助中心与常见问题FAQ', ?, '小程序端帮助中心常见问答与指引', 1)
`, [faqListVal]);
}
// 修复:faq_list 若存在但为空数组(曾被"空保存"清空),自动回填默认种子,
// 避免小程序端只显示本地兜底文案、后台却为空。
try {
const [faqRows]: any = await conn.query("SELECT value_json FROM platform_configs WHERE config_key = 'faq_list' LIMIT 1");
const raw = faqRows && faqRows[0] && faqRows[0].value_json;
let parsed: any = raw;
if (typeof raw === 'string') {
try { parsed = JSON.parse(raw); } catch (_) { parsed = null; }
}
if (!Array.isArray(parsed) || parsed.length === 0) {
await conn.query("UPDATE platform_configs SET value_json = ? WHERE config_key = 'faq_list'", [faqListVal]);
}
} catch (_) {}
if (!existingKeys.has('security_words')) {
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)
VALUES ('security_words', '私下引流与违规自定义敏感词库', ?, '全平台发帖、评论及订单防私下交易关键词实时过滤', 1)
`, [securityWordsVal]);
}
if (!existingKeys.has('home_popup')) {
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)
VALUES ('home_popup', '小程序首页运营全局弹窗', ?, '节日活动推广、新用户专享首单优惠全局浮层弹窗', 1)
`, [homePopupVal]);
}
if (!existingKeys.has('expert_rules')) {
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)
VALUES ('expert_rules', '大神技能标签库与定价区间规范', ?, '陪玩大神入驻审核、定价上下限限制与官方推荐技能标签库', 1)
`, [expertRulesVal]);
}
if (!existingKeys.has('finance_rules')) {
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)
VALUES ('finance_rules', '平台抽成与推广分销规则', ?, '平台佣金抽成比例、二级分销返润与佣金提现门槛限制', 1)
`, [financeRulesVal]);
}
if (!existingKeys.has('activity_rules')) {
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)
VALUES ('activity_rules', '同城组局发单与预算预设规范', ?, '招募席位上下限、快捷单人赏金预设与包场预算梯队', 1)
`, [activityRulesVal]);
}
if (!existingKeys.has('message_template_rules')) {
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)
VALUES ('message_template_rules', '微信服务通知与模板消息配置', ?, '微信订阅消息通知与状态模板配置', 1)
`, [messageTemplateRulesVal]);
}
if (!existingKeys.has('wechat_pay_config')) {
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)
VALUES ('wechat_pay_config', '微信支付商户号与证书配置', ?, '微信支付V3商户号、APIv3密钥与证书参数配置', 1)
`, [wechatPayConfigVal]);
} else {
const [wpRows]: any = await conn.query("SELECT value_json FROM platform_configs WHERE config_key = 'wechat_pay_config'");
if (wpRows && wpRows.length > 0) {
let curVal = wpRows[0].value_json;
if (typeof curVal === 'string') {
try { curVal = JSON.parse(curVal); } catch (_) {}
}
if (curVal && typeof curVal === 'object') {
// 一次性清理:历史版本曾在源码/DB 内置占位 appId/appSecret。
// 用标记位保证【仅执行一次】,此后绝不再改动管理员填写的值
// (即使其恰好等于占位值,例如本账真实 AppID 就是该值,也不会被误清)。
if (!existingKeys.has('wechat_pay_placeholder_cleaned')) {
let updated = false;
if (curVal.appId === 'wx0c929f7e54024e67' || curVal.appId === 'wx_peiwan_miniapp') {
curVal.appId = '';
updated = true;
}
if (curVal.appSecret === '63a412f3f73af8e73f6923943bf9683c') {
curVal.appSecret = '';
updated = true;
}
if (updated) {
await conn.query("UPDATE platform_configs SET value_json = ? WHERE config_key = 'wechat_pay_config'", [JSON.stringify(curVal)]);
}
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)
VALUES ('wechat_pay_placeholder_cleaned', '小程序凭证占位清理标记', '{"done":true}', '内部标记:仅用于一次性清理历史占位凭证,勿手动修改', 1)
`);
}
}
}
}
if (!existingKeys.has('map_location_config')) {
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)
VALUES ('map_location_config', '地图定位与LBS服务配置', ?, '腾讯位置服务Key、选点与周边测距参数配置', 1)
`, [mapLocationConfigVal]);
}
// 体验账号直登已下线:清理历史配置,避免残留测试账号清单
await conn.query("DELETE FROM platform_configs WHERE config_key = 'demo_accounts'");
if (existingKeys.has('platform_protocol')) {
const [protoRows]: any = await conn.query("SELECT value_json FROM platform_configs WHERE config_key = 'platform_protocol'");
if (protoRows && protoRows.length > 0) {
let curVal = protoRows[0].value_json;
if (typeof curVal === 'string') {
try { curVal = JSON.parse(curVal); } catch (_) {}
}
if (curVal && (!curVal.userAgreementContent || !curVal.privacyPolicyContent)) {
const defaultProto = {
userAgreementTitle: '用户服务协议',
userAgreementContent: '1. 账号使用规范:用户注册与使用同城生活陪伴服务须符合国家法律法规,严禁利用平台从事任何违法违规行为。\n2. 资金与交易保障:平台提供全额担保交易,订单完成前款项由官方账户托管,碰面出示4位核销码确认后自动结算至大神钱包。\n3. 履约安全守则:所有同城活动须在合规公开商业街区进行,严禁私下导流与脱离平台交易。\n4. 争议与售后仲裁:若遇服务不符或搭子违约,可随时在订单中发起申诉,由平台运营仲裁专员裁决。',
privacyPolicyTitle: '隐私保护指引',
privacyPolicyContent: '1. 信息收集范围:为保障同城陪玩交易安全与现场4位核销码核验,平台在获得您授权后会依法收集手机号与必要位置信息。\n2. 数据加密保护:平台全面接入微信小程序官方安全风控规范,所有用户数据均实施高强度加密脱敏存储。\n3. 权限撤回与管理:您可随时在小程序系统设置中撤回位置或通讯授权,平台未经您的明确许可绝不会向任何第三方泄露您的隐私数据。'
};
const mergedProto = {
...defaultProto,
...curVal
};
if (!mergedProto.userAgreementContent) mergedProto.userAgreementContent = defaultProto.userAgreementContent;
if (!mergedProto.privacyPolicyContent) mergedProto.privacyPolicyContent = defaultProto.privacyPolicyContent;
await conn.query("UPDATE platform_configs SET value_json = ? WHERE config_key = 'platform_protocol'", [JSON.stringify(mergedProto)]);
}
}
}
}
}