feat: update appraisal ordering and payment flows
This commit is contained in:
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);
|
||||
}
|
||||
Reference in New Issue
Block a user