feat: update appraisal ordering and payment flows
This commit is contained in:
@@ -30,11 +30,12 @@ $tables = [
|
||||
'express_companies',
|
||||
'shipping_warehouses',
|
||||
'order_shipping_targets',
|
||||
'shouqianba_payments',
|
||||
'material_tag_scan_logs', 'material_batch_download_logs', 'material_tag_codes', 'material_batches',
|
||||
'order_transfer_flow_logs', 'order_transfer_flows', 'internal_transfer_tags', 'internal_transfer_tag_batches',
|
||||
'enterprise_webhook_deliveries', 'enterprise_order_events', 'enterprise_customer_order_refs', 'enterprise_api_nonces', 'enterprise_customer_apps', 'enterprise_customers',
|
||||
'user_api_tokens', 'sms_code_logs',
|
||||
'admin_api_tokens', 'admin_role_permissions', 'admin_permissions', 'admin_role_relations', 'admin_roles', 'operation_logs', 'system_configs', 'admin_users',
|
||||
'admin_api_tokens', 'admin_role_permissions', 'admin_permissions', 'admin_role_relations', 'admin_roles', 'operation_logs', 'system_configs', 'appraisal_service_price_packages', 'admin_users',
|
||||
'ticket_messages', 'tickets',
|
||||
'user_messages', 'message_logs', 'message_rules', 'message_templates',
|
||||
'upload_template_items', 'upload_templates',
|
||||
@@ -130,10 +131,14 @@ INSERT INTO upload_template_items (id, template_id, item_code, item_name, is_req
|
||||
(33, 6, 'strap_buckle', '表带 / 表扣细节图', 1, '请拍摄表带材质、表扣刻字和连接处细节。', '', 2, 4, 1, '{$now}', '{$now}'),
|
||||
(34, 6, 'purchase_voucher', '购买凭证', 0, '如有保卡、发票或购买记录,可一并上传。', '', 2, 5, 1, '{$now}', '{$now}');
|
||||
|
||||
INSERT INTO orders (id, order_no, appraisal_no, user_id, service_mode, service_provider, payment_status, order_status, display_status, estimated_finish_time, source_channel, source_customer_id, pay_amount, paid_at, created_at, updated_at) VALUES
|
||||
(1, 'AXY202604200001', 'AXY-APP-20260420-0001', 1, 'physical', 'zhongjian', 'paid', 'pending_supplement', '等待您补充资料', '2026-04-21 18:00:00', 'mini_program', '', 199.00, '2026-04-20 09:12:00', '2026-04-20 09:12:00', '{$now}'),
|
||||
(2, 'AXY202604190012', 'AXY-APP-20260419-0012', 1, 'physical', 'anxinyan', 'paid', 'in_first_review', '鉴定师处理中', '2026-04-20 20:00:00', 'h5', '', 99.00, '2026-04-19 13:02:00', '2026-04-19 13:02:00', '{$now}'),
|
||||
(3, 'AXY202604180088', 'AXY-APP-20260418-0088', 1, 'physical', 'zhongjian', 'paid', 'completed', '报告已出具', '2026-04-18 20:00:00', 'enterprise_push', 'ENT-DEMO-001', 199.00, '2026-04-18 08:18:00', '2026-04-18 08:18:00', '{$now}');
|
||||
INSERT INTO appraisal_service_price_packages (id, service_provider, package_name, package_code, price, description, is_enabled, is_default, sort_order, created_at, updated_at) VALUES
|
||||
(1, 'anxinyan', '安心验基础套餐', 'anxinyan_basic', 99.00, '默认服务价格套餐', 1, 1, 1, '{$now}', '{$now}'),
|
||||
(2, 'zhongjian', '中检基础套餐', 'zhongjian_basic', 199.00, '默认服务价格套餐', 1, 1, 1, '{$now}', '{$now}');
|
||||
|
||||
INSERT INTO orders (id, order_no, appraisal_no, user_id, service_mode, service_provider, price_package_id, price_package_name, price_package_code, price_package_price, payment_status, order_status, display_status, estimated_finish_time, source_channel, source_customer_id, pay_amount, paid_at, created_at, updated_at) VALUES
|
||||
(1, 'AXY202604200001', 'AXY-APP-20260420-0001', 1, 'physical', 'zhongjian', 2, '中检基础套餐', 'zhongjian_basic', 199.00, 'paid', 'pending_supplement', '等待您补充资料', '2026-04-21 18:00:00', 'mini_program', '', 199.00, '2026-04-20 09:12:00', '2026-04-20 09:12:00', '{$now}'),
|
||||
(2, 'AXY202604190012', 'AXY-APP-20260419-0012', 1, 'physical', 'anxinyan', 1, '安心验基础套餐', 'anxinyan_basic', 99.00, 'paid', 'in_first_review', '鉴定师处理中', '2026-04-20 20:00:00', 'h5', '', 99.00, '2026-04-19 13:02:00', '2026-04-19 13:02:00', '{$now}'),
|
||||
(3, 'AXY202604180088', 'AXY-APP-20260418-0088', 1, 'physical', 'zhongjian', 2, '中检基础套餐', 'zhongjian_basic', 199.00, 'paid', 'completed', '报告已出具', '2026-04-18 20:00:00', 'enterprise_push', 'ENT-DEMO-001', 199.00, '2026-04-18 08:18:00', '2026-04-18 08:18:00', '{$now}');
|
||||
|
||||
INSERT INTO order_products (id, order_id, category_id, category_name, brand_id, brand_name, color, size_spec, serial_no, product_name, product_cover, created_at, updated_at) VALUES
|
||||
(1, 1, 1, '奢侈品箱包', 1, 'Louis Vuitton', '老花', 'MM', '', 'Louis Vuitton 奢侈品箱包', '', '{$now}', '{$now}'),
|
||||
@@ -294,7 +299,8 @@ INSERT INTO admin_permissions (id, name, code, module, action, created_at, updat
|
||||
(11, '管理仓库', 'warehouses.manage', 'warehouses', 'manage', '{$now}', '{$now}'),
|
||||
(12, '管理物料', 'materials.manage', 'materials', 'manage', '{$now}', '{$now}'),
|
||||
(13, '管理权限', 'access.manage', 'access', 'manage', '{$now}', '{$now}'),
|
||||
(14, '管理系统配置', 'system.manage', 'system_config', 'manage', '{$now}', '{$now}');
|
||||
(14, '管理系统配置', 'system.manage', 'system_config', 'manage', '{$now}', '{$now}'),
|
||||
(15, '管理服务价格', 'service_prices.manage', 'service_prices', 'manage', '{$now}', '{$now}');
|
||||
|
||||
INSERT INTO admin_role_permissions (id, role_id, permission_id, created_at) VALUES
|
||||
(1, 1, 1, '{$now}'),
|
||||
@@ -310,7 +316,8 @@ INSERT INTO admin_role_permissions (id, role_id, permission_id, created_at) VALU
|
||||
(11, 1, 11, '{$now}'),
|
||||
(12, 1, 12, '{$now}'),
|
||||
(13, 1, 13, '{$now}'),
|
||||
(14, 1, 14, '{$now}');
|
||||
(14, 1, 14, '{$now}'),
|
||||
(15, 1, 15, '{$now}');
|
||||
|
||||
INSERT INTO system_configs (id, config_group, config_key, config_value, remark, created_at, updated_at) VALUES
|
||||
(1, 'mini_program', 'app_id', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
@@ -320,19 +327,26 @@ INSERT INTO system_configs (id, config_group, config_key, config_value, remark,
|
||||
(5, 'h5', 'app_secret', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(6, 'h5', 'oauth_redirect_url', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(7, 'h5', 'page_base_url', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(8, 'payment', 'mch_id', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(9, 'payment', 'api_v3_key', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(10, 'payment', 'merchant_serial_no', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(11, 'payment', 'merchant_private_key', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(12, 'payment', 'platform_certificate_serial', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(13, 'payment', 'notify_url', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(14, 'sms', 'access_key_id', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(15, 'sms', 'access_key_secret', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(16, 'sms', 'sign_name', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(17, 'sms', 'login_template_code', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(18, 'sms', 'region_id', 'cn-hangzhou', '后台系统配置', '{$now}', '{$now}'),
|
||||
(19, 'sms', 'endpoint', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(20, 'user_settings', 'user_1', '{\"notify_order\":true,\"notify_report\":true,\"notify_supplement\":true,\"notify_ticket\":true,\"marketing_notify\":false,\"privacy_mode\":false}', '用户端设置偏好', '{$now}', '{$now}');
|
||||
(8, 'payment', 'enabled', 'disabled', '后台系统配置', '{$now}', '{$now}'),
|
||||
(9, 'payment', 'api_domain', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(10, 'payment', 'appid', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(11, 'payment', 'brand_code', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(12, 'payment', 'store_sn', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(13, 'payment', 'store_name', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(14, 'payment', 'workstation_sn', '0', '后台系统配置', '{$now}', '{$now}'),
|
||||
(15, 'payment', 'industry_code', '0', '后台系统配置', '{$now}', '{$now}'),
|
||||
(16, 'payment', 'order_expire_minutes', '1440', '后台系统配置', '{$now}', '{$now}'),
|
||||
(17, 'payment', 'merchant_private_key', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(18, 'payment', 'shouqianba_public_key', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(19, 'payment', 'notify_url', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(20, 'payment', 'mini_program_plugin_version', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(21, 'sms', 'access_key_id', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(22, 'sms', 'access_key_secret', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(23, 'sms', 'sign_name', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(24, 'sms', 'login_template_code', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(25, 'sms', 'region_id', 'cn-hangzhou', '后台系统配置', '{$now}', '{$now}'),
|
||||
(26, 'sms', 'endpoint', '', '后台系统配置', '{$now}', '{$now}'),
|
||||
(27, 'user_settings', 'user_1', '{\"notify_order\":true,\"notify_report\":true,\"notify_supplement\":true,\"notify_ticket\":true,\"marketing_notify\":false,\"privacy_mode\":false}', '用户端设置偏好', '{$now}', '{$now}');
|
||||
");
|
||||
|
||||
echo "SEED_OK\n";
|
||||
|
||||
@@ -83,6 +83,19 @@ function buildH5OAuthRedirectUrl(string $pageBaseUrl): string
|
||||
return $baseUrl === '' ? '' : $baseUrl . '/#/pages/auth/login';
|
||||
}
|
||||
|
||||
function buildShouqianbaNotifyUrl(array $env): string
|
||||
{
|
||||
$baseUrl = trim((string)($env['PUBLIC_FILE_BASE_URL'] ?? ''));
|
||||
if ($baseUrl === '') {
|
||||
return '';
|
||||
}
|
||||
if (!preg_match('/^https?:\/\//i', $baseUrl)) {
|
||||
$baseUrl = 'https://' . ltrim($baseUrl, '/');
|
||||
}
|
||||
|
||||
return rtrim($baseUrl, '/') . '/api/open/shouqianba/payment/notify';
|
||||
}
|
||||
|
||||
function checkClientProductionApiBase(array &$issues, string $label, string $envPath): void
|
||||
{
|
||||
$env = @parse_ini_file($envPath);
|
||||
@@ -130,6 +143,7 @@ foreach ($configRows as $row) {
|
||||
$configMap[$row['config_group'] . '.' . $row['config_key']] = (string)($row['config_value'] ?? '');
|
||||
}
|
||||
$configMap['h5.oauth_redirect_url'] = buildH5OAuthRedirectUrl((string)($configMap['h5.page_base_url'] ?? ''));
|
||||
$configMap['payment.notify_url'] = buildShouqianbaNotifyUrl($env);
|
||||
|
||||
$requiredConfigKeys = [
|
||||
'mini_program.app_id',
|
||||
@@ -143,18 +157,34 @@ $requiredConfigKeys = [
|
||||
'sms.access_key_secret',
|
||||
'sms.sign_name',
|
||||
'sms.login_template_code',
|
||||
'payment.mch_id',
|
||||
'payment.api_v3_key',
|
||||
'payment.merchant_serial_no',
|
||||
'payment.enabled',
|
||||
'payment.api_domain',
|
||||
'payment.appid',
|
||||
'payment.brand_code',
|
||||
'payment.store_sn',
|
||||
'payment.merchant_private_key',
|
||||
'payment.platform_certificate_serial',
|
||||
'payment.shouqianba_public_key',
|
||||
'payment.notify_url',
|
||||
'payment.mini_program_plugin_version',
|
||||
];
|
||||
|
||||
foreach ($requiredConfigKeys as $key) {
|
||||
check(($configMap[$key] ?? '') !== '', $issues, 'FAIL', "系统配置缺失: {$key}", "后台系统配置中 {$key} 仍为空。");
|
||||
}
|
||||
|
||||
check(($configMap['payment.enabled'] ?? '') === 'enabled', $issues, 'FAIL', '收钱吧支付未启用', '后台系统配置 payment.enabled 必须为 enabled。');
|
||||
foreach (['payment.api_domain' => '收钱吧 API 域名', 'payment.notify_url' => '收钱吧通知地址'] as $key => $label) {
|
||||
if (($configMap[$key] ?? '') !== '') {
|
||||
check(
|
||||
preg_match('/^https?:\/\//i', (string)$configMap[$key]) === 1,
|
||||
$issues,
|
||||
'FAIL',
|
||||
"{$label} 格式不正确",
|
||||
"后台系统配置 {$key} 需以 http:// 或 https:// 开头。"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (($configMap['h5.page_base_url'] ?? '') !== '' && isPlaceholderApiBase((string)$configMap['h5.page_base_url'])) {
|
||||
add_issue($issues, 'FAIL', 'H5 页面根地址未配置正式域名', '后台系统配置 h5.page_base_url 仍为本地或占位地址,扫码公开链接将无法用于正式环境。');
|
||||
}
|
||||
@@ -162,8 +192,7 @@ if (($configMap['h5.page_base_url'] ?? '') !== '' && isPlaceholderApiBase((strin
|
||||
$demoValues = [
|
||||
'mini_program.app_id' => 'wx1234567890test',
|
||||
'h5.app_id' => 'h5_app_demo',
|
||||
'payment.mch_id' => '1900000109',
|
||||
'payment.api_v3_key' => 'demo_api_v3_key_1234567890',
|
||||
'payment.api_domain' => 'https://example.com',
|
||||
];
|
||||
|
||||
foreach ($demoValues as $key => $value) {
|
||||
@@ -211,6 +240,27 @@ if (!is_array($manifest)) {
|
||||
'manifest.json 中 mp-weixin.appid 与后台系统配置 mini_program.app_id 不一致,请先执行配置同步。'
|
||||
);
|
||||
}
|
||||
$plugin = $manifest['mp-weixin']['plugins']['lite-pos-plugin'] ?? null;
|
||||
if (!is_array($plugin)) {
|
||||
add_issue($issues, 'FAIL', '小程序未声明收钱吧插件', 'manifest.json 中缺少 mp-weixin.plugins.lite-pos-plugin,请先执行配置同步。');
|
||||
} else {
|
||||
check(
|
||||
($plugin['provider'] ?? '') === 'wx7903bb295ac26ac7',
|
||||
$issues,
|
||||
'FAIL',
|
||||
'小程序收钱吧插件 provider 不正确',
|
||||
'manifest.json 中 lite-pos-plugin.provider 必须为 wx7903bb295ac26ac7。'
|
||||
);
|
||||
if (($configMap['payment.mini_program_plugin_version'] ?? '') !== '') {
|
||||
check(
|
||||
($plugin['version'] ?? '') === $configMap['payment.mini_program_plugin_version'],
|
||||
$issues,
|
||||
'FAIL',
|
||||
'小程序收钱吧插件版本未同步后台配置',
|
||||
'manifest.json 中 lite-pos-plugin.version 与后台 payment.mini_program_plugin_version 不一致,请先执行配置同步。'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkClientProductionApiBase($issues, 'admin-web', $projectRoot . '/admin-web/.env.production');
|
||||
|
||||
133
server-api/tools/schema_upgrade_service_price_packages.php
Normal file
133
server-api/tools/schema_upgrade_service_price_packages.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
|
||||
$dotenv->safeLoad();
|
||||
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
|
||||
$_ENV['DB_HOST'] ?? '127.0.0.1',
|
||||
$_ENV['DB_PORT'] ?? '3306',
|
||||
$_ENV['DB_DATABASE'] ?? '',
|
||||
$_ENV['DB_CHARSET'] ?? 'utf8mb4'
|
||||
);
|
||||
|
||||
$pdo = new PDO(
|
||||
$dsn,
|
||||
$_ENV['DB_USERNAME'] ?? '',
|
||||
$_ENV['DB_PASSWORD'] ?? '',
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]
|
||||
);
|
||||
|
||||
function column_exists(PDO $pdo, string $table, string $column): bool
|
||||
{
|
||||
$stmt = $pdo->prepare("SHOW COLUMNS FROM {$table} LIKE ?");
|
||||
$stmt->execute([$column]);
|
||||
return (bool)$stmt->fetch();
|
||||
}
|
||||
|
||||
function add_column_if_missing(PDO $pdo, string $table, string $column, string $definition): void
|
||||
{
|
||||
if (column_exists($pdo, $table, $column)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo->exec("ALTER TABLE {$table} ADD COLUMN {$column} {$definition}");
|
||||
echo "ADD_COLUMN {$table}.{$column}\n";
|
||||
}
|
||||
|
||||
$pdo->exec(<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS appraisal_service_price_packages (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
service_provider VARCHAR(32) NOT NULL DEFAULT 'anxinyan',
|
||||
package_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
package_code VARCHAR(64) NOT NULL DEFAULT '',
|
||||
price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
description VARCHAR(500) NOT NULL DEFAULT '',
|
||||
is_enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||
is_default TINYINT(1) NOT NULL DEFAULT 0,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_appraisal_service_price_packages_code (service_provider, package_code),
|
||||
KEY idx_appraisal_service_price_packages_provider (service_provider),
|
||||
KEY idx_appraisal_service_price_packages_enabled (is_enabled)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='鉴定服务价格套餐'
|
||||
SQL);
|
||||
|
||||
add_column_if_missing($pdo, 'appraisal_drafts', 'price_package_id', 'BIGINT UNSIGNED NULL DEFAULT NULL AFTER service_provider');
|
||||
add_column_if_missing($pdo, 'appraisal_drafts', 'price_package_name', "VARCHAR(128) NOT NULL DEFAULT '' AFTER price_package_id");
|
||||
add_column_if_missing($pdo, 'appraisal_drafts', 'price_package_code', "VARCHAR(64) NOT NULL DEFAULT '' AFTER price_package_name");
|
||||
add_column_if_missing($pdo, 'appraisal_drafts', 'price_package_price', 'DECIMAL(10,2) NULL DEFAULT NULL AFTER price_package_code');
|
||||
|
||||
add_column_if_missing($pdo, 'orders', 'price_package_id', 'BIGINT UNSIGNED NULL DEFAULT NULL AFTER service_provider');
|
||||
add_column_if_missing($pdo, 'orders', 'price_package_name', "VARCHAR(128) NOT NULL DEFAULT '' AFTER price_package_id");
|
||||
add_column_if_missing($pdo, 'orders', 'price_package_code', "VARCHAR(64) NOT NULL DEFAULT '' AFTER price_package_name");
|
||||
add_column_if_missing($pdo, 'orders', 'price_package_price', 'DECIMAL(10,2) NULL DEFAULT NULL AFTER price_package_code');
|
||||
|
||||
$indexRows = $pdo->query("SHOW INDEX FROM orders WHERE Key_name = 'idx_orders_price_package_id'")->fetchAll();
|
||||
if (!$indexRows) {
|
||||
$pdo->exec('ALTER TABLE orders ADD KEY idx_orders_price_package_id (price_package_id)');
|
||||
echo "ADD_INDEX orders.idx_orders_price_package_id\n";
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$seedStmt = $pdo->prepare(
|
||||
'INSERT INTO appraisal_service_price_packages (service_provider, package_name, package_code, price, description, is_enabled, is_default, sort_order, created_at, updated_at)
|
||||
SELECT ?, ?, ?, ?, ?, 1, 1, 1, ?, ?
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM appraisal_service_price_packages WHERE service_provider = ? AND package_code = ?
|
||||
)'
|
||||
);
|
||||
$seedStmt->execute(['anxinyan', '安心验基础套餐', 'anxinyan_basic', 99.00, '默认服务价格套餐', $now, $now, 'anxinyan', 'anxinyan_basic']);
|
||||
$seedStmt->execute(['zhongjian', '中检基础套餐', 'zhongjian_basic', 199.00, '默认服务价格套餐', $now, $now, 'zhongjian', 'zhongjian_basic']);
|
||||
|
||||
foreach (['anxinyan', 'zhongjian'] as $serviceProvider) {
|
||||
$candidateStmt = $pdo->prepare('SELECT id FROM appraisal_service_price_packages WHERE service_provider = ? AND is_enabled = 1 ORDER BY is_default DESC, sort_order ASC, id ASC LIMIT 1');
|
||||
$candidateStmt->execute([$serviceProvider]);
|
||||
$candidateId = (int)$candidateStmt->fetchColumn();
|
||||
if ($candidateId > 0) {
|
||||
$clearStmt = $pdo->prepare('UPDATE appraisal_service_price_packages SET is_default = 0, updated_at = ? WHERE service_provider = ? AND id <> ? AND is_default = 1');
|
||||
$clearStmt->execute([$now, $serviceProvider, $candidateId]);
|
||||
|
||||
$defaultStmt = $pdo->prepare('UPDATE appraisal_service_price_packages SET is_default = 1, updated_at = ? WHERE id = ? AND is_default <> 1');
|
||||
$defaultStmt->execute([$now, $candidateId]);
|
||||
echo "NORMALIZE_DEFAULT_PACKAGE {$serviceProvider}:{$candidateId}\n";
|
||||
} else {
|
||||
$clearStmt = $pdo->prepare('UPDATE appraisal_service_price_packages SET is_default = 0, updated_at = ? WHERE service_provider = ? AND is_default = 1');
|
||||
$clearStmt->execute([$now, $serviceProvider]);
|
||||
}
|
||||
}
|
||||
|
||||
$permissionStmt = $pdo->prepare(
|
||||
'INSERT INTO admin_permissions (name, code, module, action, created_at, updated_at)
|
||||
SELECT ?, ?, ?, ?, ?, ?
|
||||
WHERE NOT EXISTS (SELECT 1 FROM admin_permissions WHERE code = ?)'
|
||||
);
|
||||
$permissionStmt->execute(['管理服务价格', 'service_prices.manage', 'service_prices', 'manage', $now, $now, 'service_prices.manage']);
|
||||
$permissionIdStmt = $pdo->prepare('SELECT id FROM admin_permissions WHERE code = ?');
|
||||
$permissionIdStmt->execute(['service_prices.manage']);
|
||||
$permissionId = (int)$permissionIdStmt->fetchColumn();
|
||||
|
||||
$roleIdStmt = $pdo->prepare('SELECT id FROM admin_roles WHERE code = ? LIMIT 1');
|
||||
$roleIdStmt->execute(['super_admin']);
|
||||
$roleId = (int)$roleIdStmt->fetchColumn();
|
||||
if ($permissionId > 0 && $roleId > 0) {
|
||||
$rolePermissionStmt = $pdo->prepare(
|
||||
'INSERT INTO admin_role_permissions (role_id, permission_id, created_at)
|
||||
SELECT ?, ?, ?
|
||||
WHERE NOT EXISTS (SELECT 1 FROM admin_role_permissions WHERE role_id = ? AND permission_id = ?)'
|
||||
);
|
||||
$rolePermissionStmt->execute([$roleId, $permissionId, $now, $roleId, $permissionId]);
|
||||
}
|
||||
|
||||
$pdo->exec("DELETE FROM system_configs WHERE config_group = 'appraisal_service'");
|
||||
|
||||
echo "SCHEMA_UPGRADE_SERVICE_PRICE_PACKAGES_OK\n";
|
||||
115
server-api/tools/schema_upgrade_shouqianba_payment.php
Normal file
115
server-api/tools/schema_upgrade_shouqianba_payment.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
|
||||
$dotenv->safeLoad();
|
||||
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
|
||||
$_ENV['DB_HOST'] ?? '127.0.0.1',
|
||||
$_ENV['DB_PORT'] ?? '3306',
|
||||
$_ENV['DB_DATABASE'] ?? '',
|
||||
$_ENV['DB_CHARSET'] ?? 'utf8mb4'
|
||||
);
|
||||
|
||||
$pdo = new PDO(
|
||||
$dsn,
|
||||
$_ENV['DB_USERNAME'] ?? '',
|
||||
$_ENV['DB_PASSWORD'] ?? '',
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]
|
||||
);
|
||||
|
||||
function hasTable(PDO $pdo, string $table): bool
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?');
|
||||
$stmt->execute([$table]);
|
||||
return (int)$stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
function hasSystemConfig(PDO $pdo, string $group, string $key): bool
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM system_configs WHERE config_group = ? AND config_key = ?');
|
||||
$stmt->execute([$group, $key]);
|
||||
return (int)$stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
function hasColumn(PDO $pdo, string $table, string $column): bool
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?');
|
||||
$stmt->execute([$table, $column]);
|
||||
return (int)$stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
if (hasTable($pdo, 'orders') && !hasColumn($pdo, 'orders', 'cancelled_at')) {
|
||||
$pdo->exec('ALTER TABLE orders ADD COLUMN cancelled_at DATETIME NULL DEFAULT NULL AFTER paid_at');
|
||||
echo "ADD_COLUMN orders.cancelled_at\n";
|
||||
}
|
||||
|
||||
if (!hasTable($pdo, 'shouqianba_payments')) {
|
||||
$pdo->exec(<<<'SQL'
|
||||
CREATE TABLE shouqianba_payments (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
order_id BIGINT UNSIGNED NOT NULL,
|
||||
order_no VARCHAR(64) NOT NULL,
|
||||
check_sn VARCHAR(32) NOT NULL,
|
||||
order_sn VARCHAR(32) NOT NULL DEFAULT '',
|
||||
order_token VARCHAR(64) NOT NULL DEFAULT '',
|
||||
cashier_url VARCHAR(500) NOT NULL DEFAULT '',
|
||||
order_image_url VARCHAR(500) NOT NULL DEFAULT '',
|
||||
order_landing_url VARCHAR(500) NOT NULL DEFAULT '',
|
||||
scene VARCHAR(8) NOT NULL DEFAULT '',
|
||||
source_channel VARCHAR(32) NOT NULL DEFAULT '',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||||
amount INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
currency VARCHAR(3) NOT NULL DEFAULT '156',
|
||||
request_json LONGTEXT NULL,
|
||||
response_json LONGTEXT NULL,
|
||||
notify_json LONGTEXT NULL,
|
||||
paid_at DATETIME NULL DEFAULT NULL,
|
||||
cancelled_at DATETIME NULL DEFAULT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_shouqianba_payments_check_sn (check_sn),
|
||||
KEY idx_shouqianba_payments_order_id (order_id),
|
||||
KEY idx_shouqianba_payments_order_sn (order_sn),
|
||||
KEY idx_shouqianba_payments_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='收钱吧支付流水'
|
||||
SQL);
|
||||
echo "CREATE_TABLE shouqianba_payments\n";
|
||||
}
|
||||
|
||||
$configs = [
|
||||
['payment', 'enabled', 'disabled'],
|
||||
['payment', 'api_domain', ''],
|
||||
['payment', 'appid', ''],
|
||||
['payment', 'brand_code', ''],
|
||||
['payment', 'store_sn', ''],
|
||||
['payment', 'store_name', ''],
|
||||
['payment', 'workstation_sn', '0'],
|
||||
['payment', 'industry_code', '0'],
|
||||
['payment', 'order_expire_minutes', '1440'],
|
||||
['payment', 'merchant_private_key', ''],
|
||||
['payment', 'shouqianba_public_key', ''],
|
||||
['payment', 'notify_url', ''],
|
||||
['payment', 'mini_program_plugin_version', ''],
|
||||
];
|
||||
|
||||
$stmt = $pdo->prepare('INSERT INTO system_configs (config_group, config_key, config_value, remark, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)');
|
||||
foreach ($configs as [$group, $key, $value]) {
|
||||
if (hasSystemConfig($pdo, $group, $key)) {
|
||||
continue;
|
||||
}
|
||||
$stmt->execute([$group, $key, $value, '后台系统配置', $now, $now]);
|
||||
echo "ADD_CONFIG {$group}.{$key}\n";
|
||||
}
|
||||
|
||||
echo "SCHEMA_UPGRADE_OK\n";
|
||||
388
server-api/tools/shouqianba_payment_mock_test.php
Normal file
388
server-api/tools/shouqianba_payment_mock_test.php
Normal file
@@ -0,0 +1,388 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
|
||||
$dotenv->safeLoad();
|
||||
|
||||
use app\support\ShouqianbaClient;
|
||||
use app\support\ShouqianbaConfigService;
|
||||
use app\support\ShouqianbaPaymentService;
|
||||
use support\think\Db;
|
||||
|
||||
Db::setConfig(require dirname(__DIR__) . '/config/think-orm.php');
|
||||
|
||||
class MockShouqianbaClient extends ShouqianbaClient
|
||||
{
|
||||
public array $queryData = [];
|
||||
public int $voidCalls = 0;
|
||||
|
||||
public function purchase(array $body): array
|
||||
{
|
||||
return [
|
||||
'request' => ['body' => $body],
|
||||
'response' => ['response' => ['body' => ['biz_response' => ['data' => [
|
||||
'order_sn' => 'SQBMOCKREMOTE' . substr((string)$body['check_sn'], -8),
|
||||
'order_token' => 'mock_token_' . $body['check_sn'],
|
||||
'cashier_url' => 'https://cashier.mock/' . $body['check_sn'],
|
||||
]]]]],
|
||||
'data' => [
|
||||
'order_sn' => 'SQBMOCKREMOTE' . substr((string)$body['check_sn'], -8),
|
||||
'order_token' => 'mock_token_' . $body['check_sn'],
|
||||
'cashier_url' => 'https://cashier.mock/' . $body['check_sn'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function query(array $body): array
|
||||
{
|
||||
return [
|
||||
'request' => ['body' => $body],
|
||||
'response' => ['response' => ['body' => ['biz_response' => ['data' => $this->queryData]]]],
|
||||
'data' => $this->queryData,
|
||||
];
|
||||
}
|
||||
|
||||
public function void(array $body): array
|
||||
{
|
||||
$this->voidCalls++;
|
||||
|
||||
return [
|
||||
'request' => ['body' => $body],
|
||||
'response' => ['response' => ['body' => ['biz_response' => ['data' => [
|
||||
'order_status' => '0',
|
||||
'check_sn' => (string)($body['original_check_sn'] ?? ''),
|
||||
]]]]],
|
||||
'data' => [
|
||||
'order_status' => '0',
|
||||
'check_sn' => (string)($body['original_check_sn'] ?? ''),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function decodeNotification(string $rawBody): array
|
||||
{
|
||||
$data = json_decode($rawBody, true);
|
||||
if (!is_array($data)) {
|
||||
throw new RuntimeException('mock notification json invalid');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
function assertTrue(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureConfig(string $group, string $key, string $value): void
|
||||
{
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$exists = Db::name('system_configs')
|
||||
->where('config_group', $group)
|
||||
->where('config_key', $key)
|
||||
->find();
|
||||
|
||||
$payload = [
|
||||
'config_group' => $group,
|
||||
'config_key' => $key,
|
||||
'config_value' => $value,
|
||||
'remark' => '收钱吧支付 mock 测试配置',
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
if ($exists) {
|
||||
Db::name('system_configs')->where('id', (int)$exists['id'])->update($payload);
|
||||
return;
|
||||
}
|
||||
|
||||
$payload['created_at'] = $now;
|
||||
Db::name('system_configs')->insert($payload);
|
||||
}
|
||||
|
||||
function captureConfigs(array $keys): array
|
||||
{
|
||||
$snapshot = [];
|
||||
foreach ($keys as $key) {
|
||||
[$group, $configKey] = explode('.', $key, 2);
|
||||
$row = Db::name('system_configs')
|
||||
->where('config_group', $group)
|
||||
->where('config_key', $configKey)
|
||||
->find();
|
||||
$snapshot[$key] = $row ?: null;
|
||||
}
|
||||
|
||||
return $snapshot;
|
||||
}
|
||||
|
||||
function restoreConfigs(array $snapshot): void
|
||||
{
|
||||
foreach ($snapshot as $key => $row) {
|
||||
[$group, $configKey] = explode('.', $key, 2);
|
||||
if ($row) {
|
||||
Db::name('system_configs')
|
||||
->where('config_group', $group)
|
||||
->where('config_key', $configKey)
|
||||
->update([
|
||||
'config_value' => (string)($row['config_value'] ?? ''),
|
||||
'remark' => (string)($row['remark'] ?? '后台系统配置'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
Db::name('system_configs')
|
||||
->where('config_group', $group)
|
||||
->where('config_key', $configKey)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupMockData(): void
|
||||
{
|
||||
$orderIds = Db::name('orders')->whereLike('order_no', 'SQBMOCK%')->column('id');
|
||||
if ($orderIds) {
|
||||
Db::name('shouqianba_payments')->whereIn('order_id', $orderIds)->delete();
|
||||
Db::name('message_logs')->where('biz_type', 'order')->whereIn('biz_id', $orderIds)->delete();
|
||||
Db::name('user_messages')->where('biz_type', 'order')->whereIn('biz_id', $orderIds)->delete();
|
||||
Db::name('appraisal_tasks')->whereIn('order_id', $orderIds)->delete();
|
||||
Db::name('order_timelines')->whereIn('order_id', $orderIds)->delete();
|
||||
Db::name('order_shipping_targets')->whereIn('order_id', $orderIds)->delete();
|
||||
Db::name('order_return_addresses')->whereIn('order_id', $orderIds)->delete();
|
||||
$uploadItemIds = Db::name('order_upload_items')->whereIn('order_id', $orderIds)->column('id');
|
||||
if ($uploadItemIds) {
|
||||
Db::name('order_upload_files')->whereIn('order_upload_item_id', $uploadItemIds)->delete();
|
||||
}
|
||||
Db::name('order_upload_items')->whereIn('order_id', $orderIds)->delete();
|
||||
Db::name('order_extras')->whereIn('order_id', $orderIds)->delete();
|
||||
Db::name('order_products')->whereIn('order_id', $orderIds)->delete();
|
||||
Db::name('orders')->whereIn('id', $orderIds)->delete();
|
||||
}
|
||||
|
||||
$userIds = Db::name('users')->whereLike('mobile', '1399919%')->column('id');
|
||||
if ($userIds) {
|
||||
Db::name('user_auths')->whereIn('user_id', $userIds)->delete();
|
||||
Db::name('user_addresses')->whereIn('user_id', $userIds)->delete();
|
||||
Db::name('users')->whereIn('id', $userIds)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
function createMockUser(): int
|
||||
{
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$userId = (int)Db::name('users')->insertGetId([
|
||||
'nickname' => '收钱吧支付测试用户',
|
||||
'avatar' => '',
|
||||
'mobile' => '13999190001',
|
||||
'password' => '',
|
||||
'status' => 'enabled',
|
||||
'last_login_at' => null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
Db::name('user_addresses')->insert([
|
||||
'user_id' => $userId,
|
||||
'consignee' => '测试用户',
|
||||
'mobile' => '13999190001',
|
||||
'province' => '广东省',
|
||||
'city' => '深圳市',
|
||||
'district' => '南山区',
|
||||
'detail_address' => '收钱吧测试地址',
|
||||
'is_default' => 1,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
return $userId;
|
||||
}
|
||||
|
||||
function createMockOrder(int $userId, string $suffix): int
|
||||
{
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$orderId = (int)Db::name('orders')->insertGetId([
|
||||
'order_no' => 'SQBMOCK' . $suffix,
|
||||
'appraisal_no' => 'SQB-MOCK-' . $suffix,
|
||||
'user_id' => $userId,
|
||||
'service_mode' => 'physical',
|
||||
'service_provider' => 'anxinyan',
|
||||
'payment_status' => 'unpaid',
|
||||
'order_status' => 'pending_payment',
|
||||
'display_status' => '待支付',
|
||||
'estimated_finish_time' => date('Y-m-d H:i:s', strtotime('+48 hours')),
|
||||
'source_channel' => 'h5',
|
||||
'source_customer_id' => '',
|
||||
'pay_amount' => 9.99,
|
||||
'paid_at' => null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
Db::name('order_products')->insert([
|
||||
'order_id' => $orderId,
|
||||
'category_id' => null,
|
||||
'category_name' => '测试品类',
|
||||
'brand_id' => null,
|
||||
'brand_name' => '测试品牌',
|
||||
'color' => '',
|
||||
'size_spec' => '',
|
||||
'serial_no' => '',
|
||||
'product_name' => '收钱吧支付测试商品',
|
||||
'product_cover' => '',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
Db::name('order_extras')->insert([
|
||||
'order_id' => $orderId,
|
||||
'purchase_channel' => '',
|
||||
'purchase_price' => 0,
|
||||
'purchase_date' => null,
|
||||
'usage_status' => '',
|
||||
'condition_desc' => '',
|
||||
'has_accessories' => 0,
|
||||
'accessories_json' => json_encode([], JSON_UNESCAPED_UNICODE),
|
||||
'remark' => '',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
Db::name('order_timelines')->insert([
|
||||
'order_id' => $orderId,
|
||||
'node_code' => 'created',
|
||||
'node_text' => '订单已生成',
|
||||
'node_desc' => '订单资料已保存,等待用户完成支付。',
|
||||
'operator_type' => 'system',
|
||||
'operator_id' => null,
|
||||
'occurred_at' => $now,
|
||||
'created_at' => $now,
|
||||
]);
|
||||
|
||||
return $orderId;
|
||||
}
|
||||
|
||||
function latestPayment(int $orderId): array
|
||||
{
|
||||
$payment = Db::name('shouqianba_payments')->where('order_id', $orderId)->order('id', 'desc')->find();
|
||||
assertTrue((bool)$payment, 'payment row missing');
|
||||
|
||||
return $payment;
|
||||
}
|
||||
|
||||
$configKeys = [
|
||||
'payment.enabled',
|
||||
'payment.api_domain',
|
||||
'payment.appid',
|
||||
'payment.brand_code',
|
||||
'payment.store_sn',
|
||||
'payment.store_name',
|
||||
'payment.workstation_sn',
|
||||
'payment.industry_code',
|
||||
'payment.order_expire_minutes',
|
||||
'payment.merchant_private_key',
|
||||
'payment.shouqianba_public_key',
|
||||
'payment.notify_url',
|
||||
'payment.mini_program_plugin_version',
|
||||
'h5.page_base_url',
|
||||
];
|
||||
|
||||
$snapshot = captureConfigs($configKeys);
|
||||
$client = new MockShouqianbaClient(new ShouqianbaConfigService());
|
||||
$service = new ShouqianbaPaymentService(null, $client);
|
||||
|
||||
try {
|
||||
cleanupMockData();
|
||||
|
||||
ensureConfig('payment', 'enabled', 'enabled');
|
||||
ensureConfig('payment', 'api_domain', 'https://mock.shouqianba.test');
|
||||
ensureConfig('payment', 'appid', 'sqb_mock_appid');
|
||||
ensureConfig('payment', 'brand_code', 'mock_brand');
|
||||
ensureConfig('payment', 'store_sn', 'mock_store');
|
||||
ensureConfig('payment', 'store_name', 'mock store');
|
||||
ensureConfig('payment', 'workstation_sn', '0');
|
||||
ensureConfig('payment', 'industry_code', '0');
|
||||
ensureConfig('payment', 'order_expire_minutes', '1440');
|
||||
ensureConfig('payment', 'merchant_private_key', "-----BEGIN PRIVATE KEY-----\nmock\n-----END PRIVATE KEY-----");
|
||||
ensureConfig('payment', 'shouqianba_public_key', "-----BEGIN PUBLIC KEY-----\nmock\n-----END PUBLIC KEY-----");
|
||||
ensureConfig('payment', 'notify_url', 'https://api.example.com/api/open/shouqianba/payment/notify');
|
||||
ensureConfig('payment', 'mini_program_plugin_version', '2.3.70');
|
||||
ensureConfig('h5', 'page_base_url', 'https://m.example.com');
|
||||
|
||||
$userId = createMockUser();
|
||||
|
||||
$notifyOrderId = createMockOrder($userId, 'NOTIFY');
|
||||
$launch = $service->createOrReusePayment($notifyOrderId);
|
||||
assertTrue(($launch['status'] ?? '') === 'pending', 'purchase should create pending payment');
|
||||
assertTrue(($launch['cashier_url'] ?? '') !== '', 'purchase cashier_url missing');
|
||||
$payment = latestPayment($notifyOrderId);
|
||||
$notifyPayload = [
|
||||
'check_sn' => $payment['check_sn'],
|
||||
'order_status' => '4',
|
||||
'amount' => (int)$payment['amount'],
|
||||
'order_sn' => 'SQBMOCKPAID',
|
||||
];
|
||||
$service->handleNotification(json_encode($notifyPayload, JSON_UNESCAPED_UNICODE));
|
||||
$service->handleNotification(json_encode($notifyPayload, JSON_UNESCAPED_UNICODE));
|
||||
$paidOrder = Db::name('orders')->where('id', $notifyOrderId)->find();
|
||||
assertTrue(($paidOrder['payment_status'] ?? '') === 'paid', 'notify should mark order paid');
|
||||
assertTrue(($paidOrder['order_status'] ?? '') === 'pending_shipping', 'notify should move order to pending_shipping');
|
||||
assertTrue((int)Db::name('appraisal_tasks')->where('order_id', $notifyOrderId)->count() === 1, 'notify should be idempotent for appraisal task');
|
||||
assertTrue((int)Db::name('order_timelines')->where('order_id', $notifyOrderId)->where('node_code', 'payment_paid')->count() === 1, 'notify should be idempotent for paid timeline');
|
||||
|
||||
$queryOrderId = createMockOrder($userId, 'QUERY');
|
||||
$service->createOrReusePayment($queryOrderId);
|
||||
$queryPayment = latestPayment($queryOrderId);
|
||||
$client->queryData = [
|
||||
'check_sn' => $queryPayment['check_sn'],
|
||||
'order_status' => '4',
|
||||
'amount' => (int)$queryPayment['amount'],
|
||||
'order_sn' => 'SQBMOCKQUERYPAID',
|
||||
];
|
||||
$sync = $service->syncOrderPaymentStatus($queryOrderId, $userId);
|
||||
assertTrue(($sync['payment_status'] ?? '') === 'paid', 'query sync should mark paid');
|
||||
|
||||
$cancelOrderId = createMockOrder($userId, 'CANCEL');
|
||||
$service->createOrReusePayment($cancelOrderId);
|
||||
$cancelPayment = latestPayment($cancelOrderId);
|
||||
$client->queryData = [
|
||||
'check_sn' => $cancelPayment['check_sn'],
|
||||
'order_status' => '1',
|
||||
'amount' => (int)$cancelPayment['amount'],
|
||||
'order_sn' => 'SQBMOCKCANCELPENDING',
|
||||
];
|
||||
$cancel = $service->cancelPendingOrder($cancelOrderId, $userId);
|
||||
assertTrue(($cancel['order_status'] ?? '') === 'cancelled', 'cancel should mark order cancelled');
|
||||
assertTrue($client->voidCalls === 1, 'cancel should call remote void once');
|
||||
|
||||
$paidCancelOrderId = createMockOrder($userId, 'PAIDCANCEL');
|
||||
$service->createOrReusePayment($paidCancelOrderId);
|
||||
$paidCancelPayment = latestPayment($paidCancelOrderId);
|
||||
$client->queryData = [
|
||||
'check_sn' => $paidCancelPayment['check_sn'],
|
||||
'order_status' => '4',
|
||||
'amount' => (int)$paidCancelPayment['amount'],
|
||||
'order_sn' => 'SQBMOCKPAIDCANCEL',
|
||||
];
|
||||
$thrown = false;
|
||||
try {
|
||||
$service->cancelPendingOrder($paidCancelOrderId, $userId);
|
||||
} catch (RuntimeException $e) {
|
||||
$thrown = str_contains($e->getMessage(), '已支付');
|
||||
}
|
||||
$paidCancelOrder = Db::name('orders')->where('id', $paidCancelOrderId)->find();
|
||||
assertTrue($thrown, 'cancel paid remote order should be rejected');
|
||||
assertTrue(($paidCancelOrder['payment_status'] ?? '') === 'paid', 'cancel paid remote order should sync paid state');
|
||||
|
||||
echo "SHOUQIANBA_PAYMENT_MOCK_TEST_OK\n";
|
||||
} catch (Throwable $e) {
|
||||
fwrite(STDERR, "SHOUQIANBA_PAYMENT_MOCK_TEST_FAIL: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
} finally {
|
||||
cleanupMockData();
|
||||
restoreConfigs($snapshot);
|
||||
}
|
||||
@@ -9,6 +9,74 @@ $dotenv->safeLoad();
|
||||
|
||||
$projectRoot = dirname(__DIR__, 2);
|
||||
$manifestPath = $projectRoot . '/user-app/src/manifest.json';
|
||||
$litePosPluginProvider = 'wx7903bb295ac26ac7';
|
||||
$litePosPluginExport = 'miniprogram_npm/lite-pos-plugin-mate/utils.js';
|
||||
|
||||
function replaceMpWeixinPlugin(string $content, string $version, string $provider, string $exportPath): string
|
||||
{
|
||||
if (!preg_match('/"mp-weixin"\s*:\s*\{/', $content, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
throw new RuntimeException('未在 manifest.json 中找到 mp-weixin 配置块。');
|
||||
}
|
||||
|
||||
$blockStart = $matches[0][1] + strlen($matches[0][0]) - 1;
|
||||
$length = strlen($content);
|
||||
$depth = 0;
|
||||
$inString = false;
|
||||
$escaped = false;
|
||||
$blockEnd = -1;
|
||||
for ($i = $blockStart; $i < $length; $i++) {
|
||||
$char = $content[$i];
|
||||
if ($inString) {
|
||||
if ($escaped) {
|
||||
$escaped = false;
|
||||
continue;
|
||||
}
|
||||
if ($char === '\\') {
|
||||
$escaped = true;
|
||||
continue;
|
||||
}
|
||||
if ($char === '"') {
|
||||
$inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($char === '"') {
|
||||
$inString = true;
|
||||
continue;
|
||||
}
|
||||
if ($char === '{') {
|
||||
$depth++;
|
||||
continue;
|
||||
}
|
||||
if ($char === '}') {
|
||||
$depth--;
|
||||
if ($depth === 0) {
|
||||
$blockEnd = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($blockEnd < 0) {
|
||||
throw new RuntimeException('manifest.json 中 mp-weixin 配置块不完整。');
|
||||
}
|
||||
|
||||
$block = substr($content, $blockStart, $blockEnd - $blockStart + 1);
|
||||
$block = preg_replace('/,\s*"plugins"\s*:\s*\{\s*"lite-pos-plugin"\s*:\s*\{.*?\}\s*\}/s', '', $block, 1);
|
||||
if (!is_string($block)) {
|
||||
throw new RuntimeException('清理 manifest.json 小程序插件配置失败。');
|
||||
}
|
||||
|
||||
$pluginBlock = sprintf(
|
||||
",\n \"plugins\" : {\n \"lite-pos-plugin\" : {\n \"version\" : \"%s\",\n \"provider\" : \"%s\",\n \"export\" : \"%s\"\n }\n }",
|
||||
addcslashes($version, "\\\""),
|
||||
addcslashes($provider, "\\\""),
|
||||
addcslashes($exportPath, "\\\"")
|
||||
);
|
||||
$updatedBlock = rtrim(substr($block, 0, -1)) . $pluginBlock . "\n }";
|
||||
|
||||
return substr($content, 0, $blockStart) . $updatedBlock . substr($content, $blockEnd + 1);
|
||||
}
|
||||
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
|
||||
@@ -39,6 +107,10 @@ try {
|
||||
if ($miniProgramAppId === '') {
|
||||
throw new RuntimeException('后台系统配置 mini_program.app_id 为空,无法同步到 user-app manifest。');
|
||||
}
|
||||
$litePosPluginVersion = trim($configMap['payment.mini_program_plugin_version'] ?? '');
|
||||
if ($litePosPluginVersion === '') {
|
||||
throw new RuntimeException('后台系统配置 payment.mini_program_plugin_version 为空,无法同步收钱吧小程序插件。');
|
||||
}
|
||||
|
||||
$manifestContent = @file_get_contents($manifestPath);
|
||||
if ($manifestContent === false) {
|
||||
@@ -60,6 +132,7 @@ try {
|
||||
if (!is_string($updatedContent) || $updatedContent === '') {
|
||||
throw new RuntimeException('同步 manifest.json 失败。');
|
||||
}
|
||||
$updatedContent = replaceMpWeixinPlugin($updatedContent, $litePosPluginVersion, $litePosPluginProvider, $litePosPluginExport);
|
||||
|
||||
if (@file_put_contents($manifestPath, $updatedContent) === false) {
|
||||
throw new RuntimeException('写入 user-app/src/manifest.json 失败。');
|
||||
@@ -67,6 +140,7 @@ try {
|
||||
|
||||
echo "SYNC_CLIENT_CONFIG_OK\n";
|
||||
echo "mini_program.app_id => {$miniProgramAppId}\n";
|
||||
echo "payment.mini_program_plugin_version => {$litePosPluginVersion}\n";
|
||||
} catch (Throwable $e) {
|
||||
fwrite(STDERR, "SYNC_CLIENT_CONFIG_FAIL: {$e->getMessage()}\n");
|
||||
exit(1);
|
||||
|
||||
Reference in New Issue
Block a user