feat: security hardening, wechat pay v3 platform cert integration, companion certification revocation, and audit closed-loop
这个提交包含在:
@@ -27,6 +27,7 @@ export async function initDatabase(): Promise<void> {
|
||||
title VARCHAR(255) NOT NULL,
|
||||
game_category VARCHAR(64) NOT NULL,
|
||||
location VARCHAR(255),
|
||||
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,
|
||||
@@ -38,6 +39,12 @@ export async function initDatabase(): Promise<void> {
|
||||
) 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 status VARCHAR(32) DEFAULT 'RECRUITING' AFTER price_adjust_rule`);
|
||||
} catch (_) {
|
||||
@@ -57,10 +64,24 @@ export async function initDatabase(): Promise<void> {
|
||||
verified_by INT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_activity (activity_id),
|
||||
INDEX idx_verify_code (verify_code)
|
||||
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 (
|
||||
@@ -131,7 +152,9 @@ export async function initDatabase(): Promise<void> {
|
||||
"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 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"
|
||||
];
|
||||
for (const colSql of extraOrderCols) {
|
||||
try {
|
||||
@@ -194,6 +217,26 @@ export async function initDatabase(): Promise<void> {
|
||||
await conn.query('ALTER TABLE withdrawals ADD COLUMN reason VARCHAR(255) 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;
|
||||
`);
|
||||
|
||||
// 8. 线下合规场所库
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS safety_places (
|
||||
@@ -292,10 +335,18 @@ export async function initDatabase(): Promise<void> {
|
||||
reply_time DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_order (order_id),
|
||||
INDEX idx_expert (expert_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 (_) {}
|
||||
@@ -895,6 +946,32 @@ async function seedInitialDataIfEmpty(conn: any): Promise<void> {
|
||||
remark: '微信小程序订阅消息与服务通知模版ID配置'
|
||||
});
|
||||
|
||||
const wechatPayConfigVal = JSON.stringify({
|
||||
mode: 'AUTO',
|
||||
appId: 'wx0c929f7e54024e67',
|
||||
appSecret: '63a412f3f73af8e73f6923943bf9683c',
|
||||
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 与微信小程序 AppID: wx0c929f7e54024e67)'
|
||||
});
|
||||
|
||||
const [cfgItemRows]: any = await conn.query('SELECT COUNT(*) as count FROM platform_configs');
|
||||
if (cfgItemRows[0].count === 0) {
|
||||
const customerServiceVal = JSON.stringify({
|
||||
@@ -938,12 +1015,16 @@ async function seedInitialDataIfEmpty(conn: any): Promise<void> {
|
||||
('home_popup', '小程序首页运营全局弹窗', ?, '节日活动推广、新用户专享首单优惠全局浮层弹窗', 1),
|
||||
('expert_rules', '大神技能标签库与定价区间规范', ?, '陪玩大神入驻审核、定价上下限限制与官方推荐技能标签库', 1),
|
||||
('finance_rules', '平台抽成与推广分销规则', ?, '平台佣金抽成比例、二级分销返润与佣金提现门槛限制', 1),
|
||||
('activity_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
|
||||
financeRulesVal, activityRulesVal, messageTemplateRulesVal,
|
||||
wechatPayConfigVal, mapLocationConfigVal
|
||||
]);
|
||||
} else {
|
||||
// 增量保证各核心配置项完整就绪
|
||||
@@ -1003,6 +1084,40 @@ async function seedInitialDataIfEmpty(conn: any): Promise<void> {
|
||||
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') {
|
||||
let updated = false;
|
||||
if (!curVal.appId || curVal.appId === 'wx_peiwan_miniapp') {
|
||||
curVal.appId = 'wx0c929f7e54024e67';
|
||||
updated = true;
|
||||
}
|
||||
if (!curVal.appSecret) {
|
||||
curVal.appSecret = '63a412f3f73af8e73f6923943bf9683c';
|
||||
updated = true;
|
||||
}
|
||||
if (updated) {
|
||||
await conn.query("UPDATE platform_configs SET value_json = ? WHERE config_key = 'wechat_pay_config'", [JSON.stringify(curVal)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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]);
|
||||
}
|
||||
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) {
|
||||
|
||||
文件差异内容过多而无法显示
加载差异
在新工单中引用
屏蔽一个用户