chore: prepare anxinyan release
This commit is contained in:
@@ -51,6 +51,31 @@ function isPlaceholderApiBase(string $apiBase): bool
|
||||
return str_contains($normalized, 'example.com');
|
||||
}
|
||||
|
||||
function normalizeH5PageBaseUrl(string $value): string
|
||||
{
|
||||
$baseUrl = trim($value);
|
||||
if ($baseUrl === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$hashPos = strpos($baseUrl, '#');
|
||||
if ($hashPos !== false) {
|
||||
$baseUrl = substr($baseUrl, 0, $hashPos);
|
||||
}
|
||||
|
||||
if (!preg_match('/^https?:\/\//i', $baseUrl)) {
|
||||
$baseUrl = 'https://' . ltrim($baseUrl, '/');
|
||||
}
|
||||
|
||||
return rtrim($baseUrl, '/');
|
||||
}
|
||||
|
||||
function buildH5OAuthRedirectUrl(string $pageBaseUrl): string
|
||||
{
|
||||
$baseUrl = normalizeH5PageBaseUrl($pageBaseUrl);
|
||||
return $baseUrl === '' ? '' : $baseUrl . '/#/pages/auth/login';
|
||||
}
|
||||
|
||||
function checkClientProductionApiBase(array &$issues, string $label, string $envPath): void
|
||||
{
|
||||
$env = @parse_ini_file($envPath);
|
||||
@@ -97,6 +122,7 @@ $configMap = [];
|
||||
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'] ?? ''));
|
||||
|
||||
$requiredConfigKeys = [
|
||||
'mini_program.app_id',
|
||||
|
||||
66
server-api/tools/schema_upgrade_wechat_h5_auth.php
Normal file
66
server-api/tools/schema_upgrade_wechat_h5_auth.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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 hasIndex(PDO $pdo, string $table, string $index): bool
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?');
|
||||
$stmt->execute([$table, $index]);
|
||||
return (int)$stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
if (!hasTable($pdo, 'user_auths')) {
|
||||
$pdo->exec(<<<'SQL'
|
||||
CREATE TABLE user_auths (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
auth_type VARCHAR(32) NOT NULL,
|
||||
auth_open_id VARCHAR(128) NOT NULL DEFAULT '',
|
||||
auth_union_id VARCHAR(128) NOT NULL DEFAULT '',
|
||||
auth_key VARCHAR(128) NOT NULL DEFAULT '',
|
||||
auth_extra JSON 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_user_auths_type_key (auth_type, auth_key),
|
||||
KEY idx_user_auths_user_id (user_id),
|
||||
KEY idx_user_auths_auth_union_id (auth_type, auth_union_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户认证映射'
|
||||
SQL);
|
||||
echo "CREATE_TABLE user_auths\n";
|
||||
} elseif (!hasIndex($pdo, 'user_auths', 'idx_user_auths_auth_union_id')) {
|
||||
$pdo->exec('ALTER TABLE user_auths ADD KEY idx_user_auths_auth_union_id (auth_type, auth_union_id)');
|
||||
echo "ADD_INDEX user_auths.idx_user_auths_auth_union_id\n";
|
||||
}
|
||||
|
||||
echo "SCHEMA_UPGRADE_OK\n";
|
||||
206
server-api/tools/wechat_h5_auth_mock_test.php
Normal file
206
server-api/tools/wechat_h5_auth_mock_test.php
Normal file
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
|
||||
$dotenv->safeLoad();
|
||||
|
||||
$_ENV['APP_DEBUG'] = 'true';
|
||||
$_ENV['WECHAT_H5_AUTH_MOCK'] = 'true';
|
||||
|
||||
use app\support\AppAuthService;
|
||||
use support\think\Db;
|
||||
use Webman\Http\Request;
|
||||
|
||||
Db::setConfig(require dirname(__DIR__) . '/config/think-orm.php');
|
||||
|
||||
function assertTrue(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
function makeRequest(): Request
|
||||
{
|
||||
return new class("GET /api/app/auth/wechat/mock-test HTTP/1.1\r\nHost: 127.0.0.1\r\nUser-Agent: wechat-h5-auth-mock-test\r\n\r\n") extends Request {
|
||||
public function getRealIp(bool $safeMode = true): string
|
||||
{
|
||||
return '127.0.0.1';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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' => '微信 H5 授权测试配置',
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
if ($exists) {
|
||||
Db::name('system_configs')->where('id', $exists['id'])->update($payload);
|
||||
return;
|
||||
}
|
||||
|
||||
$payload['created_at'] = $now;
|
||||
Db::name('system_configs')->insert($payload);
|
||||
}
|
||||
|
||||
function latestDebugCode(string $mobile): string
|
||||
{
|
||||
$record = Db::name('sms_code_logs')
|
||||
->where('mobile', $mobile)
|
||||
->where('scene', 'login')
|
||||
->where('send_status', 'mock')
|
||||
->whereNull('used_at')
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
assertTrue((bool)$record, 'mock sms code record missing');
|
||||
|
||||
for ($code = 100000; $code <= 999999; $code++) {
|
||||
$codeText = (string)$code;
|
||||
if (hash_equals((string)$record['code_hash'], hash('sha256', implode('|', [$mobile, 'login', $codeText])))) {
|
||||
return $codeText;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException('mock sms code not found');
|
||||
}
|
||||
|
||||
function cleanupWechatMockData(): void
|
||||
{
|
||||
$userIds = Db::name('users')
|
||||
->whereLike('mobile', '1399900%')
|
||||
->column('id');
|
||||
if ($userIds) {
|
||||
Db::name('user_api_tokens')->whereIn('user_id', $userIds)->delete();
|
||||
Db::name('user_auths')->whereIn('user_id', $userIds)->delete();
|
||||
Db::name('users')->whereIn('id', $userIds)->delete();
|
||||
}
|
||||
|
||||
Db::name('sms_code_logs')->whereLike('mobile', '1399900%')->delete();
|
||||
Db::name('user_auths')->where('auth_type', 'wechat_h5')->whereLike('auth_key', 'mock_openid_%')->delete();
|
||||
}
|
||||
|
||||
$service = new AppAuthService();
|
||||
$request = makeRequest();
|
||||
$originalConfigs = [
|
||||
'h5.app_id' => Db::name('system_configs')->where('config_group', 'h5')->where('config_key', 'app_id')->value('config_value'),
|
||||
'h5.app_secret' => Db::name('system_configs')->where('config_group', 'h5')->where('config_key', 'app_secret')->value('config_value'),
|
||||
'h5.page_base_url' => Db::name('system_configs')->where('config_group', 'h5')->where('config_key', 'page_base_url')->value('config_value'),
|
||||
];
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
cleanupWechatMockData();
|
||||
ensureConfig('h5', 'app_id', 'wx_mock_appid');
|
||||
ensureConfig('h5', 'app_secret', 'mock_secret');
|
||||
ensureConfig('h5', 'page_base_url', 'https://m.example.com');
|
||||
ensureConfig('sms', 'access_key_id', '');
|
||||
ensureConfig('sms', 'access_key_secret', '');
|
||||
ensureConfig('sms', 'sign_name', '');
|
||||
ensureConfig('sms', 'login_template_code', '');
|
||||
|
||||
$config = $service->wechatConfig();
|
||||
assertTrue($config['enabled'] === true, 'wechat config should be enabled');
|
||||
assertTrue($config['oauth_redirect_url'] === 'https://m.example.com/#/pages/auth/login', 'oauth redirect url mismatch');
|
||||
|
||||
$stateOne = (string)$config['state'];
|
||||
$exchange = $service->exchangeWechatCode('mock_newuser', $stateOne, $request);
|
||||
assertTrue(($exchange['status'] ?? '') === 'need_bind', 'new wechat user should need binding');
|
||||
assertTrue(!empty($exchange['bind_ticket']), 'bind ticket missing');
|
||||
|
||||
$mobile = '13999000001';
|
||||
$service->sendLoginCode($mobile, $request);
|
||||
$bind = $service->bindWechatMobile((string)$exchange['bind_ticket'], $mobile, latestDebugCode($mobile), $request);
|
||||
assertTrue(($bind['status'] ?? '') === 'logged_in' && !empty($bind['token']), 'bind should return token');
|
||||
|
||||
$stateTwo = (string)$service->wechatConfig()['state'];
|
||||
$linked = $service->exchangeWechatCode('mock_newuser', $stateTwo, $request);
|
||||
assertTrue(($linked['status'] ?? '') === 'logged_in' && !empty($linked['token']), 'linked wechat user should login');
|
||||
|
||||
$stateThree = (string)$service->wechatConfig()['state'];
|
||||
$existingUser = $service->exchangeWechatCode('mock_existingmobile', $stateThree, $request);
|
||||
$existingMobile = '13999000002';
|
||||
Db::name('users')->insert([
|
||||
'nickname' => '已有手机号用户',
|
||||
'avatar' => '',
|
||||
'mobile' => $existingMobile,
|
||||
'password' => '',
|
||||
'status' => 'enabled',
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
$service->sendLoginCode($existingMobile, $request);
|
||||
$boundExisting = $service->bindWechatMobile((string)$existingUser['bind_ticket'], $existingMobile, latestDebugCode($existingMobile), $request);
|
||||
assertTrue(($boundExisting['status'] ?? '') === 'logged_in', 'existing mobile should bind');
|
||||
assertTrue((int)Db::name('users')->where('mobile', $existingMobile)->count() === 1, 'existing mobile should not duplicate user');
|
||||
|
||||
$otherUserId = (int)Db::name('users')->insertGetId([
|
||||
'nickname' => '占用微信用户',
|
||||
'avatar' => '',
|
||||
'mobile' => '13999000003',
|
||||
'password' => '',
|
||||
'status' => 'enabled',
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
Db::name('user_auths')->insert([
|
||||
'user_id' => $otherUserId,
|
||||
'auth_type' => 'wechat_h5',
|
||||
'auth_key' => 'mock_openid_conflict',
|
||||
'auth_open_id' => 'mock_openid_conflict',
|
||||
'auth_union_id' => 'mock_unionid_conflict',
|
||||
'auth_extra' => json_encode(['mock' => true]),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
$conflictUser = $service->exchangeWechatCode('mock_conflict', (string)$service->wechatConfig()['state'], $request);
|
||||
assertTrue(($conflictUser['status'] ?? '') === 'logged_in', 'existing conflicting openid should login owner directly');
|
||||
|
||||
try {
|
||||
$service->exchangeWechatCode('mock_expired', (string)$service->wechatConfig()['state'], $request);
|
||||
throw new RuntimeException('expired code should fail');
|
||||
} catch (RuntimeException $e) {
|
||||
assertTrue(strpos($e->getMessage(), 'code 无效') !== false, 'expired code message mismatch');
|
||||
}
|
||||
|
||||
try {
|
||||
$service->bindWechatMobile('invalid.ticket', '13999000004', '123456', $request);
|
||||
throw new RuntimeException('invalid bind ticket should fail');
|
||||
} catch (RuntimeException $e) {
|
||||
assertTrue(strpos($e->getMessage(), '绑定凭证') !== false, 'invalid bind ticket message mismatch');
|
||||
}
|
||||
|
||||
try {
|
||||
$service->exchangeWechatCode('mock_statefail', 'wrongstate', $request);
|
||||
throw new RuntimeException('invalid state should fail');
|
||||
} catch (RuntimeException $e) {
|
||||
assertTrue(strpos($e->getMessage(), '状态') !== false, 'invalid state message mismatch');
|
||||
}
|
||||
|
||||
echo "WECHAT_H5_AUTH_MOCK_TEST_OK\n";
|
||||
Db::rollback();
|
||||
} catch (Throwable $e) {
|
||||
Db::rollback();
|
||||
fwrite(STDERR, "WECHAT_H5_AUTH_MOCK_TEST_FAIL: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
} finally {
|
||||
foreach ($originalConfigs as $mapKey => $value) {
|
||||
[$group, $key] = explode('.', $mapKey, 2);
|
||||
ensureConfig($group, $key, (string)$value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user