<?php
declare(strict_types=1);

require dirname(__DIR__) . '/app/bootstrap.php';

$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$path = routePath();

if ($method === 'GET' && ($path === '/health' || $path === '/v1/health')) {
    db()->query('SELECT 1');
    responseJson(['status' => 'ready', 'version' => '1.0.0', 'runtime' => 'php']);
}

if ($method === 'GET' && $path === '/v1/app/config') {
    $rows = db()->query('SELECT config_key,config_value,value_type FROM system_configs WHERE is_secret=0 ORDER BY id')->fetchAll();
    $items = [];
    foreach ($rows as $row) {
        $value = $row['config_value'];
        if ($row['value_type'] === 'int') $value = (int)$value;
        if ($row['value_type'] === 'bool') $value = strtolower((string)$value) === 'true';
        $items[$row['config_key']] = $value;
    }
    responseJson(['items' => $items]);
}

if ($method === 'POST' && $path === '/v1/auth/register') {
    $body = requestBody();
    $phone = trim((string)($body['phone'] ?? ''));
    $password = (string)($body['password'] ?? '');
    $nickname = trim((string)($body['nickname'] ?? ''));
    if (!preg_match('/^1[3-9]\d{9}$/', $phone)) fail('请输入正确的11位手机号');
    if (strlen($password) < 8 || strlen($password) > 72) fail('密码需要8至72位');
    if (mb_strlen($nickname, 'UTF-8') < 2 || mb_strlen($nickname, 'UTF-8') > 20) fail('昵称需要2至20个字');
    rateLimit(clientIp(), 'register', 10, 3600);
    $hash = phoneHash($phone);
    $stmt = db()->prepare('SELECT id FROM users WHERE phone_hash=? LIMIT 1');
    $stmt->execute([$hash]);
    if ($stmt->fetch()) fail('该手机号已注册', 409, 'phone_exists');
    $publicId = randomCode('U', 6);
    $nickname = filterText($nickname);
    $stmt = db()->prepare('INSERT INTO users(public_id,phone_ciphertext,phone_hash,password_hash,nickname,account_status,created_at,updated_at) VALUES(?,?,?,?,?,1,NOW(),NOW())');
    $stmt->execute([$publicId, encryptValue($phone), $hash, password_hash($password, PASSWORD_DEFAULT), $nickname]);
    $userId = (int)db()->lastInsertId();
    db()->prepare('INSERT INTO wallet_accounts(user_id,updated_at) VALUES(?,NOW())')->execute([$userId]);
    db()->prepare('INSERT INTO user_daily_activity(user_id,activity_date,first_seen_at,last_seen_at) VALUES(?,CURDATE(),NOW(),NOW())')->execute([$userId]);
    $token = issueUserSession($userId);
    responseJson(['token' => $token, 'user' => ['public_id' => $publicId, 'nickname' => $nickname]], 201, '注册成功');
}

if ($method === 'POST' && $path === '/v1/auth/login') {
    $body = requestBody();
    $phone = trim((string)($body['phone'] ?? ''));
    $password = (string)($body['password'] ?? '');
    rateLimit(clientIp(), 'login', 30, 600);
    $stmt = db()->prepare('SELECT * FROM users WHERE phone_hash=? AND deleted_at IS NULL LIMIT 1');
    $stmt->execute([phoneHash($phone)]);
    $user = $stmt->fetch();
    if (!$user || !password_verify($password, $user['password_hash'])) fail('手机号或密码不正确', 401, 'invalid_credentials');
    if ((int)$user['account_status'] !== 1) fail('账号当前不可登录，请联系客服', 403, 'account_restricted');
    db()->prepare('UPDATE users SET last_login_at=NOW(),updated_at=NOW() WHERE id=?')->execute([$user['id']]);
    db()->prepare('INSERT INTO user_daily_activity(user_id,activity_date,first_seen_at,last_seen_at) VALUES(?,CURDATE(),NOW(),NOW()) ON DUPLICATE KEY UPDATE last_seen_at=NOW()')->execute([$user['id']]);
    $token = issueUserSession((int)$user['id']);
    responseJson(['token' => $token, 'user' => ['public_id' => $user['public_id'], 'nickname' => $user['nickname']]], 200, '登录成功');
}

if ($method === 'POST' && $path === '/v1/auth/logout') {
    requireUser();
    $token = bearerToken();
    db()->prepare('UPDATE user_sessions SET revoked_at=NOW() WHERE token_hash=?')->execute([tokenHash((string)$token)]);
    responseJson(null, 200, '已退出登录');
}

if ($method === 'GET' && $path === '/v1/categories') {
    $items = db()->query('SELECT id,code,name,description,icon,color_hex FROM group_categories WHERE enabled=1 ORDER BY sort_order,id')->fetchAll();
    responseJson(['items' => $items]);
}

if ($method === 'GET' && $path === '/v1/groups') {
    $keyword = trim((string)($_GET['keyword'] ?? ''));
    $categoryId = max(0, (int)($_GET['category_id'] ?? 0));
    $sql = 'SELECT g.id,g.public_id,g.name,g.avatar_url,g.summary,g.description,g.city,g.join_type,g.price_cents,g.requires_review,g.member_limit,g.member_count,g.verified_badge,c.name AS category_name,c.icon AS category_icon FROM chat_groups g JOIN group_categories c ON c.id=g.category_id WHERE g.review_status=3 AND g.deleted_at IS NULL';
    $params = [];
    if ($categoryId > 0) { $sql .= ' AND g.category_id=?'; $params[] = $categoryId; }
    if ($keyword !== '') { $sql .= ' AND (g.name LIKE ? OR g.summary LIKE ? OR g.city LIKE ?)'; $like = '%' . $keyword . '%'; array_push($params, $like, $like, $like); }
    $sql .= ' ORDER BY g.verified_badge DESC,g.member_count DESC,g.id DESC LIMIT 100';
    $stmt = db()->prepare($sql);
    $stmt->execute($params);
    responseJson(['items' => $stmt->fetchAll()]);
}

if ($method === 'GET' && preg_match('#^/v1/groups/(\d+)$#', $path, $match)) {
    $stmt = db()->prepare('SELECT g.*,c.name AS category_name,u.nickname AS owner_name FROM chat_groups g JOIN group_categories c ON c.id=g.category_id JOIN users u ON u.id=g.owner_user_id WHERE g.id=? AND g.review_status=3 AND g.deleted_at IS NULL');
    $stmt->execute([(int)$match[1]]);
    $group = $stmt->fetch();
    if (!$group) fail('群聊不存在或已下架', 404, 'group_not_found');
    responseJson($group);
}

if ($method === 'POST' && $path === '/v1/groups') {
    $user = requireUser();
    rateLimit('user:' . $user['id'], 'create_group', 5, 86400);
    $body = requestBody();
    $name = filterText(trim((string)($body['name'] ?? '')));
    $summary = filterText(trim((string)($body['summary'] ?? '')));
    $description = filterText(trim((string)($body['description'] ?? '')));
    $categoryId = (int)($body['category_id'] ?? 0);
    $joinType = (int)($body['join_type'] ?? 1);
    $price = max(0, (int)($body['price_cents'] ?? 0));
    if (mb_strlen($name, 'UTF-8') < 2 || mb_strlen($name, 'UTF-8') > 80 || $summary === '' || $categoryId < 1) fail('群聊名称、分类或简介不正确');
    if (!in_array($joinType, [1,2,3], true) || ($joinType === 2 && $price < 100)) fail('进群方式或价格不正确，付费群最低1元');
    $stmt = db()->prepare('SELECT id FROM group_categories WHERE id=? AND enabled=1'); $stmt->execute([$categoryId]);
    if (!$stmt->fetch()) fail('群分类不存在');
    $publicId = randomCode('G', 6); $pdo = db(); $pdo->beginTransaction();
    try {
        $stmt = $pdo->prepare('INSERT INTO chat_groups(public_id,owner_user_id,category_id,name,summary,description,city,join_type,price_cents,requires_review,member_limit,member_count,review_status,verified_badge,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,1,2,0,NOW(),NOW())');
        $stmt->execute([$publicId, $user['id'], $categoryId, $name, $summary, $description, trim((string)($body['city'] ?? $user['city'] ?? '全国')), $joinType, $joinType === 2 ? $price : 0, !empty($body['requires_review']) ? 1 : 0, max(20, min(2000, (int)($body['member_limit'] ?? 500)))]);
        $groupId = (int)$pdo->lastInsertId();
        $pdo->prepare('INSERT INTO group_members(group_id,user_id,role,status,joined_at) VALUES(?,?,3,1,NOW())')->execute([$groupId, $user['id']]);
        $pdo->commit();
        responseJson(['id' => $groupId, 'public_id' => $publicId, 'review_status' => 2], 201, '群聊已提交平台审核');
    } catch (Throwable $error) { if ($pdo->inTransaction()) $pdo->rollBack(); throw $error; }
}

if ($method === 'POST' && preg_match('#^/v1/groups/(\d+)/join$#', $path, $match)) {
    $user = requireUser();
    $body = requestBody();
    $groupId = (int)$match[1];
    rateLimit('user:' . $user['id'], 'join_group', 20, 86400);
    $stmt = db()->prepare('SELECT * FROM chat_groups WHERE id=? AND review_status=3 AND deleted_at IS NULL');
    $stmt->execute([$groupId]);
    $group = $stmt->fetch();
    if (!$group) fail('群聊不存在或已下架', 404);
    $stmt = db()->prepare('SELECT status FROM group_members WHERE group_id=? AND user_id=? LIMIT 1');
    $stmt->execute([$groupId, $user['id']]);
    $membership = $stmt->fetch();
    if ($membership && (int)$membership['status'] === 1) responseJson(['joined' => true], 200, '你已经在群聊中');
    $stmt = db()->prepare('SELECT id,order_id FROM group_join_requests WHERE group_id=? AND user_id=? AND status=1 ORDER BY id DESC LIMIT 1');
    $stmt->execute([$groupId, $user['id']]);
    if ($stmt->fetch()) responseJson(['joined' => false, 'review_required' => true, 'duplicate' => true], 200, '入群申请正在审核中');
    if ((int)$group['member_count'] >= (int)$group['member_limit']) fail('群聊人数已满', 409, 'group_full');

    if ((int)$group['join_type'] === 1) {
        $pdo = db();
        $pdo->beginTransaction();
        try {
            if ((int)$group['requires_review'] === 1) {
                $stmt = $pdo->prepare('INSERT INTO group_join_requests(group_id,user_id,message,status,created_at) VALUES(?,?,?,1,NOW())');
                $stmt->execute([$groupId, $user['id'], filterText(trim((string)($body['message'] ?? '申请加入')))]);
                $result = ['joined' => false, 'review_required' => true];
            } else {
                $stmt = $pdo->prepare('INSERT INTO group_members(group_id,user_id,role,status,joined_at) VALUES(?,?,1,1,NOW()) ON DUPLICATE KEY UPDATE status=1,left_at=NULL,joined_at=NOW()');
                $stmt->execute([$groupId, $user['id']]);
                $pdo->prepare('UPDATE chat_groups SET member_count=(SELECT COUNT(*) FROM group_members WHERE group_id=? AND status=1),updated_at=NOW() WHERE id=?')->execute([$groupId, $groupId]);
                $result = ['joined' => true, 'review_required' => false];
            }
            $pdo->commit();
            responseJson($result, 200, $result['joined'] ? '进群成功' : '申请已提交');
        } catch (Throwable $error) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            throw $error;
        }
    }

    if ((int)$group['join_type'] === 3) {
        $stmt = db()->prepare('INSERT INTO group_join_requests(group_id,user_id,message,status,created_at) VALUES(?,?,?,1,NOW())');
        $stmt->execute([$groupId, $user['id'], filterText(trim((string)($body['message'] ?? '申请加入')))]);
        responseJson(['joined' => false, 'review_required' => true], 201, '申请已提交');
    }

    $channel = (string)($body['channel'] ?? 'wechat_app');
    if (!in_array($channel, ['wechat_app', 'alipay_app'], true)) fail('支付渠道不支持');
    $stmt = db()->prepare('SELECT o.order_no,p.payment_no,p.amount_cents,p.channel FROM orders o JOIN payments p ON p.order_id=o.id WHERE o.user_id=? AND o.group_id=? AND o.status=1 AND p.status=1 AND o.created_at>=DATE_SUB(NOW(),INTERVAL 30 MINUTE) ORDER BY o.id DESC LIMIT 1');
    $stmt->execute([$user['id'], $groupId]); $pendingPayment = $stmt->fetch();
    if ($pendingPayment) {
        $providerStmt = db()->prepare('SELECT public_config,enabled,environment FROM provider_configs WHERE provider_type="payment" AND provider_code=?');
        $providerStmt->execute([$pendingPayment['channel']]); $pendingProvider = $providerStmt->fetch() ?: ['public_config'=>'{}','enabled'=>0,'environment'=>'sandbox'];
        responseJson(['joined'=>false,'payment_required'=>true,'duplicate'=>true,'order_no'=>$pendingPayment['order_no'],'payment_no'=>$pendingPayment['payment_no'],'amount_cents'=>(int)$pendingPayment['amount_cents'],'channel'=>$pendingPayment['channel'],'provider_enabled'=>(bool)$pendingProvider['enabled'],'provider_environment'=>$pendingProvider['environment'],'provider_config'=>json_decode((string)$pendingProvider['public_config'],true) ?: []], 200, '请继续完成已有订单支付');
    }
    $feePercent = max(0, min(50, (int)configValue('payment.platform_fee_percent', 10)));
    $amount = (int)$group['price_cents'];
    $fee = (int)floor($amount * $feePercent / 100);
    $orderNo = randomCode('QO', 8);
    $paymentNo = randomCode('QP', 8);
    $pdo = db();
    $pdo->beginTransaction();
    try {
        $stmt = $pdo->prepare('INSERT INTO orders(order_no,user_id,group_id,owner_user_id,title,amount_cents,platform_fee_cents,owner_income_cents,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,1,NOW(),NOW())');
        $stmt->execute([$orderNo, $user['id'], $groupId, $group['owner_user_id'], '加入群聊：' . $group['name'], $amount, $fee, $amount - $fee]);
        $orderId = (int)$pdo->lastInsertId();
        $stmt = $pdo->prepare('INSERT INTO payments(order_id,payment_no,channel,amount_cents,status,created_at,updated_at) VALUES(?,?,?,?,1,NOW(),NOW())');
        $stmt->execute([$orderId, $paymentNo, $channel, $amount]);
        $pdo->commit();
    } catch (Throwable $error) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        throw $error;
    }
    $stmt = db()->prepare('SELECT public_config,enabled,environment FROM provider_configs WHERE provider_type="payment" AND provider_code=?');
    $stmt->execute([$channel]);
    $provider = $stmt->fetch() ?: ['public_config' => '{}', 'enabled' => 0, 'environment' => 'sandbox'];
    responseJson([
        'joined' => false,
        'payment_required' => true,
        'order_no' => $orderNo,
        'payment_no' => $paymentNo,
        'amount_cents' => $amount,
        'channel' => $channel,
        'provider_enabled' => (bool)$provider['enabled'],
        'provider_environment' => $provider['environment'],
        'provider_config' => json_decode((string)$provider['public_config'], true) ?: [],
    ], 201, '订单已创建');
}

if ($method === 'GET' && $path === '/v1/orders') {
    $user = requireUser();
    $stmt = db()->prepare('SELECT o.order_no,o.title,o.amount_cents,o.status,o.paid_at,o.guarantee_end_at,o.created_at,p.channel,p.status AS payment_status,g.name AS group_name FROM orders o JOIN chat_groups g ON g.id=o.group_id LEFT JOIN payments p ON p.order_id=o.id WHERE o.user_id=? ORDER BY o.id DESC LIMIT 200');
    $stmt->execute([$user['id']]);
    responseJson(['items' => $stmt->fetchAll()]);
}

if ($method === 'GET' && preg_match('#^/v1/orders/([A-Z0-9]+)$#', $path, $match)) {
    $user = requireUser();
    $stmt = db()->prepare('SELECT o.order_no,o.title,o.amount_cents,o.status,o.paid_at,o.guarantee_end_at,p.payment_no,p.channel,p.status AS payment_status FROM orders o LEFT JOIN payments p ON p.order_id=o.id WHERE o.order_no=? AND o.user_id=? LIMIT 1');
    $stmt->execute([$match[1], $user['id']]);
    $order = $stmt->fetch();
    if (!$order) fail('订单不存在', 404);
    responseJson($order);
}

if ($method === 'POST' && preg_match('#^/v1/payment/webhooks/(wechat_app|alipay_app)$#', $path, $match)) {
    if (!array_key_exists('request_raw', $GLOBALS)) {
        $GLOBALS['request_raw'] = file_get_contents('php://input');
    }
    $raw = (string)$GLOBALS['request_raw'];
    $signature = (string)($_SERVER['HTTP_X_PAYMENT_SIGNATURE'] ?? '');
    $expected = hash_hmac('sha256', $raw, requiredSecret('security.payment_callback_secret', 32));
    if ($signature === '' || !hash_equals($expected, $signature)) fail('支付回调签名无效', 401);
    $body = requestBody();
    $paymentNo = (string)($body['payment_no'] ?? '');
    $tradeNo = (string)($body['trade_no'] ?? '');
    $paidAmount = (int)($body['amount_cents'] ?? -1);
    $paymentStatus = (string)($body['status'] ?? '');
    if ($paymentNo === '' || $tradeNo === '' || $paidAmount < 0 || $paymentStatus !== 'success') fail('支付回调参数缺失或交易未成功');
    $pdo = db();
    $pdo->beginTransaction();
    try {
        $stmt = $pdo->prepare('SELECT p.*,o.order_no,o.group_id,o.user_id,o.owner_user_id,o.owner_income_cents,o.status AS order_status,g.requires_review FROM payments p JOIN orders o ON o.id=p.order_id JOIN chat_groups g ON g.id=o.group_id WHERE p.payment_no=? AND p.channel=? FOR UPDATE');
        $stmt->execute([$paymentNo, $match[1]]);
        $payment = $stmt->fetch();
        if (!$payment) fail('支付单不存在', 404);
        if ((int)$payment['status'] === 2) { $pdo->commit(); responseJson(['accepted' => true, 'duplicate' => true]); }
        if ((int)$payment['amount_cents'] !== $paidAmount) fail('支付金额校验失败', 409);
        $callbackHash = hash('sha256', $raw);
        $pdo->prepare('UPDATE payments SET status=2,channel_trade_no=?,callback_hash=?,paid_at=NOW(),updated_at=NOW() WHERE id=?')->execute([$tradeNo, $callbackHash, $payment['id']]);
        $guaranteeHours = max(1, (int)configValue('payment.guarantee_hours', 24));
        $guaranteeAt = date('Y-m-d H:i:s', time() + $guaranteeHours * 3600);
        $pdo->prepare('UPDATE orders SET status=2,paid_at=NOW(),guarantee_end_at=?,updated_at=NOW() WHERE id=?')->execute([$guaranteeAt, $payment['order_id']]);
        if ((int)$payment['requires_review'] === 1) {
            $pdo->prepare('INSERT INTO group_join_requests(group_id,user_id,order_id,message,status,created_at) VALUES(?,?,?,"付费后等待审核",1,NOW())')->execute([$payment['group_id'], $payment['user_id'], $payment['order_id']]);
        } else {
            $pdo->prepare('INSERT INTO group_members(group_id,user_id,role,status,joined_at) VALUES(?,?,1,1,NOW()) ON DUPLICATE KEY UPDATE status=1,left_at=NULL,joined_at=NOW()')->execute([$payment['group_id'], $payment['user_id']]);
            $pdo->prepare('UPDATE chat_groups SET member_count=(SELECT COUNT(*) FROM group_members WHERE group_id=? AND status=1),updated_at=NOW() WHERE id=?')->execute([$payment['group_id'], $payment['group_id']]);
        }
        $pdo->prepare('INSERT INTO wallet_accounts(user_id,updated_at) VALUES(?,NOW()) ON DUPLICATE KEY UPDATE updated_at=VALUES(updated_at)')->execute([$payment['owner_user_id']]);
        $stmt = $pdo->prepare('SELECT id FROM wallet_accounts WHERE user_id=?');
        $stmt->execute([$payment['owner_user_id']]);
        $accountId = (int)$stmt->fetchColumn();
        $pendingHours = max(1, (int)configValue('wallet.pending_hours', 168));
        $availableAt = date('Y-m-d H:i:s', time() + $pendingHours * 3600);
        $pdo->prepare('INSERT INTO wallet_pending_funds(account_id,order_id,pending_cents,available_at,status,created_at,updated_at) VALUES(?,?,?,?,1,NOW(),NOW())')->execute([$accountId, $payment['order_id'], $payment['owner_income_cents'], $availableAt]);
        $pdo->prepare('UPDATE wallet_accounts SET pending_cents=pending_cents+?,total_income_cents=total_income_cents+?,updated_at=NOW() WHERE id=?')->execute([$payment['owner_income_cents'], $payment['owner_income_cents'], $accountId]);
        $balanceStmt = $pdo->prepare('SELECT pending_cents FROM wallet_accounts WHERE id=?'); $balanceStmt->execute([$accountId]);
        $pdo->prepare('INSERT IGNORE INTO wallet_ledger(account_id,biz_type,biz_no,direction,amount_cents,balance_after_cents,remark,created_at) VALUES(?,"group_income_pending",?,1,?,?,"付费进群收入进入审核中",NOW())')->execute([$accountId, $payment['order_no'], $payment['owner_income_cents'], (int)$balanceStmt->fetchColumn()]);
        $pdo->commit();
        responseJson(['accepted' => true, 'duplicate' => false]);
    } catch (Throwable $error) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        throw $error;
    }
}

if ($method === 'POST' && preg_match('#^/v1/membership/payment/webhooks/(wechat_app|alipay_app)$#', $path, $match)) {
    if (!array_key_exists('request_raw', $GLOBALS)) $GLOBALS['request_raw'] = file_get_contents('php://input');
    $raw = (string)$GLOBALS['request_raw'];
    $signature = (string)($_SERVER['HTTP_X_PAYMENT_SIGNATURE'] ?? '');
    $expected = hash_hmac('sha256', $raw, requiredSecret('security.payment_callback_secret', 32));
    if ($signature === '' || !hash_equals($expected, $signature)) fail('支付回调签名无效', 401);
    $body = requestBody(); $paymentNo = (string)($body['payment_no'] ?? ''); $tradeNo = (string)($body['trade_no'] ?? '');
    $paidAmount = (int)($body['amount_cents'] ?? -1);
    if ($paymentNo === '' || $tradeNo === '' || $paidAmount < 0 || (string)($body['status'] ?? '') !== 'success') fail('支付回调参数缺失或交易未成功');
    $pdo = db(); $pdo->beginTransaction();
    try {
        $stmt = $pdo->prepare('SELECT p.*,o.user_id,o.plan_id,o.status AS order_status,mp.billing_period FROM membership_payments p JOIN membership_orders o ON o.id=p.membership_order_id JOIN membership_plans mp ON mp.id=o.plan_id WHERE p.payment_no=? AND p.channel=? FOR UPDATE');
        $stmt->execute([$paymentNo, $match[1]]); $payment = $stmt->fetch();
        if (!$payment) fail('会员支付单不存在', 404);
        if ((int)$payment['status'] === 2) { $pdo->commit(); responseJson(['accepted' => true, 'duplicate' => true]); }
        if ((int)$payment['amount_cents'] !== $paidAmount) fail('支付金额校验失败', 409);
        $pdo->prepare('UPDATE membership_payments SET status=2,channel_trade_no=?,callback_hash=?,paid_at=NOW(),updated_at=NOW() WHERE id=?')->execute([$tradeNo, hash('sha256', $raw), $payment['id']]);
        $pdo->prepare('UPDATE membership_orders SET status=2,paid_at=NOW(),updated_at=NOW() WHERE id=?')->execute([$payment['membership_order_id']]);
        $months = $payment['billing_period'] === 'year' ? 12 : 1;
        $stmt = $pdo->prepare('SELECT id,expires_at FROM user_memberships WHERE user_id=? AND status=1 ORDER BY expires_at DESC LIMIT 1 FOR UPDATE'); $stmt->execute([$payment['user_id']]); $current = $stmt->fetch();
        $base = $current && strtotime($current['expires_at']) > time() ? $current['expires_at'] : date('Y-m-d H:i:s');
        $expires = date('Y-m-d H:i:s', strtotime($base . ' +' . $months . ' months'));
        $pdo->prepare('UPDATE user_memberships SET status=2,updated_at=NOW() WHERE user_id=? AND status=1')->execute([$payment['user_id']]);
        $pdo->prepare('INSERT INTO user_memberships(user_id,plan_id,membership_order_id,status,starts_at,expires_at,created_at,updated_at) VALUES(?,?,?,1,NOW(),?,NOW(),NOW())')->execute([$payment['user_id'], $payment['plan_id'], $payment['membership_order_id'], $expires]);
        $pdo->prepare('UPDATE users SET is_vip=1,vip_expires_at=?,updated_at=NOW() WHERE id=?')->execute([$expires, $payment['user_id']]);
        $pdo->commit(); responseJson(['accepted' => true, 'expires_at' => $expires]);
    } catch (Throwable $error) { if ($pdo->inTransaction()) $pdo->rollBack(); throw $error; }
}

if ($method === 'POST' && preg_match('#^/v1/orders/([A-Z0-9]+)/refunds$#', $path, $match)) {
    $user = requireUser();
    $body = requestBody();
    $reason = filterText(trim((string)($body['reason'] ?? '')));
    if (mb_strlen($reason, 'UTF-8') < 5) fail('请填写至少5个字的退款原因');
    $stmt = db()->prepare('SELECT * FROM orders WHERE order_no=? AND user_id=? AND status IN (2,3) LIMIT 1');
    $stmt->execute([$match[1], $user['id']]);
    $order = $stmt->fetch();
    if (!$order) fail('订单不存在或当前不能退款', 409);
    $refundNo = randomCode('QR', 8);
    $stmt = db()->prepare('INSERT INTO refunds(refund_no,order_id,applicant_user_id,reason,amount_cents,status,created_at) VALUES(?,?,?,?,?,1,NOW())');
    $stmt->execute([$refundNo, $order['id'], $user['id'], $reason, $order['amount_cents']]);
    db()->prepare('UPDATE orders SET status=4,updated_at=NOW() WHERE id=?')->execute([$order['id']]);
    responseJson(['refund_no' => $refundNo, 'status' => 1], 201, '退款申请已提交');
}

if ($method === 'GET' && $path === '/v1/me') {
    $user = requireUser();
    $isVip = (int)$user['is_vip'] === 1 && ($user['vip_expires_at'] === null || strtotime($user['vip_expires_at']) > time());
    responseJson([
        'public_id' => $user['public_id'], 'nickname' => $user['nickname'], 'avatar_url' => $user['avatar_url'],
        'gender' => (int)$user['gender'], 'city' => $user['city'], 'bio' => $user['bio'],
        'realname_status' => (int)$user['realname_status'], 'is_vip' => $isVip, 'vip_expires_at' => $user['vip_expires_at'],
    ]);
}

if ($method === 'GET' && $path === '/v1/me/groups') {
    $user = requireUser();
    $stmt = db()->prepare('SELECT g.id,g.public_id,g.name,g.avatar_url,g.summary,g.city,g.member_count,g.verified_badge,c.name AS category_name,m.role,m.status,m.mute_until,m.joined_at FROM group_members m JOIN chat_groups g ON g.id=m.group_id JOIN group_categories c ON c.id=g.category_id WHERE m.user_id=? AND m.status IN (1,2) AND g.review_status=3 AND g.deleted_at IS NULL ORDER BY m.joined_at DESC');
    $stmt->execute([$user['id']]);
    responseJson(['items' => $stmt->fetchAll()]);
}

if ($method === 'GET' && $path === '/v1/me/owned-groups') {
    $user = requireUser();
    $stmt = db()->prepare('SELECT g.id,g.public_id,g.name,g.summary,g.city,g.join_type,g.price_cents,g.requires_review,g.member_count,g.member_limit,g.review_status,c.name AS category_name,(SELECT COUNT(*) FROM group_join_requests r WHERE r.group_id=g.id AND r.status=1) AS pending_requests FROM chat_groups g JOIN group_categories c ON c.id=g.category_id WHERE g.owner_user_id=? AND g.deleted_at IS NULL ORDER BY g.id DESC');
    $stmt->execute([$user['id']]);
    responseJson(['items' => $stmt->fetchAll()]);
}

if ($method === 'GET' && preg_match('#^/v1/owned-groups/(\d+)/join-requests$#', $path, $match)) {
    $user = requireUser();
    $groupId = (int)$match[1];
    $stmt = db()->prepare('SELECT id FROM chat_groups WHERE id=? AND owner_user_id=? AND deleted_at IS NULL');
    $stmt->execute([$groupId, $user['id']]);
    if (!$stmt->fetch()) fail('你不是该群群主', 403);
    $stmt = db()->prepare('SELECT r.id,r.message,r.status,r.created_at,u.public_id,u.nickname,u.avatar_url,u.realname_status,o.order_no,o.amount_cents FROM group_join_requests r JOIN users u ON u.id=r.user_id LEFT JOIN orders o ON o.id=r.order_id WHERE r.group_id=? ORDER BY r.id DESC LIMIT 200');
    $stmt->execute([$groupId]);
    responseJson(['items' => $stmt->fetchAll()]);
}

if ($method === 'POST' && preg_match('#^/v1/owned-groups/(\d+)/join-requests/(\d+)/(approve|reject)$#', $path, $match)) {
    $owner = requireUser();
    $groupId = (int)$match[1]; $requestId = (int)$match[2]; $approve = $match[3] === 'approve';
    $pdo = db(); $pdo->beginTransaction();
    try {
        $stmt = $pdo->prepare('SELECT r.*,g.owner_user_id,o.order_no,o.amount_cents,o.status AS order_status FROM group_join_requests r JOIN chat_groups g ON g.id=r.group_id LEFT JOIN orders o ON o.id=r.order_id WHERE r.id=? AND r.group_id=? FOR UPDATE');
        $stmt->execute([$requestId, $groupId]); $request = $stmt->fetch();
        if (!$request || (int)$request['owner_user_id'] !== (int)$owner['id']) fail('申请不存在或无权处理', 403);
        if ((int)$request['status'] !== 1) fail('申请已经处理', 409);
        if ($approve) {
            $pdo->prepare('UPDATE group_join_requests SET status=2,reviewed_at=NOW() WHERE id=?')->execute([$requestId]);
            $pdo->prepare('INSERT INTO group_members(group_id,user_id,role,status,joined_at) VALUES(?,?,1,1,NOW()) ON DUPLICATE KEY UPDATE status=1,left_at=NULL,joined_at=NOW()')->execute([$groupId, $request['user_id']]);
            $pdo->prepare('UPDATE chat_groups SET member_count=(SELECT COUNT(*) FROM group_members WHERE group_id=? AND status=1),updated_at=NOW() WHERE id=?')->execute([$groupId, $groupId]);
        } else {
            $pdo->prepare('UPDATE group_join_requests SET status=3,reviewed_at=NOW() WHERE id=?')->execute([$requestId]);
            if ($request['order_id'] !== null && in_array((int)$request['order_status'], [2,3], true)) {
                $refundNo = randomCode('QR', 8);
                $pdo->prepare('INSERT INTO refunds(refund_no,order_id,applicant_user_id,reason,amount_cents,status,created_at) VALUES(?,?,?,?,?,1,NOW())')->execute([$refundNo, $request['order_id'], $request['user_id'], '入群审核未通过，系统发起退款', $request['amount_cents']]);
                $pdo->prepare('UPDATE orders SET status=4,updated_at=NOW() WHERE id=?')->execute([$request['order_id']]);
            }
        }
        $pdo->commit();
        responseJson(['status' => $approve ? 2 : 3], 200, $approve ? '已通过入群申请' : '已拒绝并进入退款流程');
    } catch (Throwable $error) { if ($pdo->inTransaction()) $pdo->rollBack(); throw $error; }
}

if ($method === 'PATCH' && $path === '/v1/me') {
    $user = requireUser();
    $body = requestBody();
    $nickname = filterText(trim((string)($body['nickname'] ?? $user['nickname'])));
    $bio = filterText(trim((string)($body['bio'] ?? $user['bio'] ?? '')));
    $city = trim((string)($body['city'] ?? $user['city'] ?? ''));
    if (mb_strlen($nickname, 'UTF-8') < 2 || mb_strlen($nickname, 'UTF-8') > 20) fail('昵称需要2至20个字');
    if (mb_strlen($bio, 'UTF-8') > 160) fail('个人简介不能超过160字');
    db()->prepare('UPDATE users SET nickname=?,bio=?,city=?,updated_at=NOW() WHERE id=?')->execute([$nickname, $bio, $city, $user['id']]);
    responseJson(['nickname' => $nickname, 'bio' => $bio, 'city' => $city], 200, '资料已更新');
}

if ($method === 'GET' && $path === '/v1/wallet') {
    $user = requireUser();
    releaseDueFunds((int)$user['id']);
    $stmt = db()->prepare('SELECT pending_cents,available_cents,frozen_cents,total_income_cents,updated_at FROM wallet_accounts WHERE user_id=?');
    $stmt->execute([$user['id']]);
    $wallet = $stmt->fetch() ?: ['pending_cents' => 0, 'available_cents' => 0, 'frozen_cents' => 0, 'total_income_cents' => 0];
    $stmt = db()->prepare('SELECT o.order_no,o.title,p.pending_cents,p.refunded_cents,p.available_at,p.status FROM wallet_pending_funds p JOIN wallet_accounts w ON w.id=p.account_id JOIN orders o ON o.id=p.order_id WHERE w.user_id=? ORDER BY p.id DESC LIMIT 100');
    $stmt->execute([$user['id']]);
    responseJson(['account' => $wallet, 'pending_items' => $stmt->fetchAll()]);
}

if ($method === 'GET' && $path === '/v1/withdrawals') {
    $user = requireUser();
    $stmt = db()->prepare('SELECT withdrawal_no,amount_cents,account_name,status,review_note,created_at,reviewed_at,completed_at FROM withdrawal_requests WHERE user_id=? ORDER BY id DESC LIMIT 100');
    $stmt->execute([$user['id']]);
    responseJson(['items' => $stmt->fetchAll()]);
}

if ($method === 'POST' && $path === '/v1/withdrawals') {
    $user = requireUser();
    releaseDueFunds((int)$user['id']);
    $body = requestBody();
    $amount = (int)($body['amount_cents'] ?? 0);
    $accountName = trim((string)($body['account_name'] ?? ''));
    $accountNo = trim((string)($body['account_no'] ?? ''));
    if ($amount < 100) fail('提现金额至少1元');
    if ($accountName === '' || $accountNo === '') fail('请填写收款账户');
    $pdo = db();
    $pdo->beginTransaction();
    try {
        $stmt = $pdo->prepare('SELECT * FROM wallet_accounts WHERE user_id=? FOR UPDATE');
        $stmt->execute([$user['id']]);
        $wallet = $stmt->fetch();
        if (!$wallet || (int)$wallet['available_cents'] < $amount) fail('可提现余额不足', 409);
        $no = randomCode('QW', 8);
        $pdo->prepare('UPDATE wallet_accounts SET available_cents=available_cents-?,frozen_cents=frozen_cents+?,updated_at=NOW() WHERE id=?')->execute([$amount, $amount, $wallet['id']]);
        $pdo->prepare('INSERT INTO withdrawal_requests(withdrawal_no,user_id,amount_cents,account_name,account_no,status,created_at) VALUES(?,?,?,?,?,1,NOW())')->execute([$no, $user['id'], $amount, $accountName, encryptValue($accountNo)]);
        $pdo->prepare('INSERT INTO wallet_ledger(account_id,biz_type,biz_no,direction,amount_cents,balance_after_cents,remark,created_at) VALUES(?,"withdraw_freeze",?,3,?,?,"提现申请冻结",NOW())')->execute([$wallet['id'], $no, $amount, (int)$wallet['available_cents'] - $amount]);
        $pdo->commit();
        responseJson(['withdrawal_no' => $no, 'status' => 1], 201, '提现申请已提交');
    } catch (Throwable $error) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        throw $error;
    }
}

if ($method === 'GET' && preg_match('#^/v1/groups/(\d+)/members$#', $path, $match)) {
    $user = requireUser(); $groupId = (int)$match[1];
    $stmt = db()->prepare('SELECT id FROM group_members WHERE group_id=? AND user_id=? AND status IN (1,2)'); $stmt->execute([$groupId, $user['id']]);
    if (!$stmt->fetch()) fail('加入群聊后才能查看成员', 403);
    $stmt = db()->prepare('SELECT u.public_id,u.nickname,u.avatar_url,u.city,u.bio,u.realname_status,u.is_vip,m.role,m.status,m.joined_at FROM group_members m JOIN users u ON u.id=m.user_id WHERE m.group_id=? AND m.status IN (1,2) AND u.account_status=1 ORDER BY m.role DESC,m.id ASC LIMIT 1000');
    $stmt->execute([$groupId]); responseJson(['items' => $stmt->fetchAll()]);
}

if ($method === 'GET' && preg_match('#^/v1/groups/(\d+)/messages$#', $path, $match)) {
    $user = requireUser();
    $groupId = (int)$match[1];
    $stmt = db()->prepare('SELECT id,status,mute_until FROM group_members WHERE group_id=? AND user_id=? AND status IN (1,2)');
    $stmt->execute([$groupId, $user['id']]);
    if (!$stmt->fetch()) fail('加入群聊后才能查看消息', 403);
    $stmt = db()->prepare('SELECT m.id,m.message_no,m.content,m.message_type,m.created_at,u.public_id,u.nickname,u.avatar_url,u.is_vip FROM chat_messages m JOIN users u ON u.id=m.sender_user_id WHERE m.conversation_type=2 AND m.conversation_id=? AND m.deleted_at IS NULL ORDER BY m.id DESC LIMIT 100');
    $stmt->execute([$groupId]);
    responseJson(['items' => array_reverse($stmt->fetchAll())]);
}

if ($method === 'POST' && preg_match('#^/v1/groups/(\d+)/messages$#', $path, $match)) {
    $user = requireUser();
    $groupId = (int)$match[1];
    $stmt = db()->prepare('SELECT status,mute_until FROM group_members WHERE group_id=? AND user_id=? LIMIT 1');
    $stmt->execute([$groupId, $user['id']]);
    $member = $stmt->fetch();
    if (!$member || !in_array((int)$member['status'], [1,2], true)) fail('你不是该群成员', 403);
    if ((int)$member['status'] === 2 && ($member['mute_until'] === null || strtotime($member['mute_until']) > time())) fail('你当前处于禁言状态', 403);
    $limit = max(1, (int)configValue('chat.messages_per_minute', 20));
    rateLimit('user:' . $user['id'], 'chat_message', $limit, 60);
    $content = trim((string)(requestBody()['content'] ?? ''));
    $maxChars = max(10, (int)configValue('chat.message_max_chars', 500));
    if ($content === '' || mb_strlen($content, 'UTF-8') > $maxChars) fail('消息为空或超过字数限制');
    $content = filterText($content);
    $messageNo = randomCode('QM', 8);
    db()->prepare('INSERT INTO chat_messages(message_no,conversation_type,conversation_id,sender_user_id,message_type,content,review_status,created_at) VALUES(?,2,?,?,"text",?,1,NOW())')->execute([$messageNo, $groupId, $user['id'], $content]);
    responseJson(['message_no' => $messageNo, 'content' => $content, 'created_at' => date('Y-m-d H:i:s')], 201, '发送成功');
}

if ($method === 'GET' && $path === '/v1/friends') {
    $user = requireUser();
    $stmt = db()->prepare('SELECT u.public_id,u.nickname,u.avatar_url,u.city,u.is_vip,f.updated_at FROM friendships f JOIN users u ON u.id=IF(f.requester_id=?,f.addressee_id,f.requester_id) WHERE (f.requester_id=? OR f.addressee_id=?) AND f.status=2 ORDER BY f.updated_at DESC');
    $stmt->execute([$user['id'], $user['id'], $user['id']]);
    responseJson(['items' => $stmt->fetchAll()]);
}

if ($method === 'GET' && $path === '/v1/friends/requests') {
    $user = requireUser();
    $stmt = db()->prepare('SELECT f.id,f.message,f.source_group_id,f.created_at,u.public_id,u.nickname,u.avatar_url,u.city,u.is_vip FROM friendships f JOIN users u ON u.id=f.requester_id WHERE f.addressee_id=? AND f.status=1 ORDER BY f.id DESC LIMIT 100');
    $stmt->execute([$user['id']]);
    responseJson(['items' => $stmt->fetchAll()]);
}

if ($method === 'POST' && $path === '/v1/friends/requests') {
    $user = requireUser();
    $body = requestBody();
    $targetPublicId = trim((string)($body['target_public_id'] ?? ''));
    $sourceGroupId = isset($body['source_group_id']) ? (int)$body['source_group_id'] : null;
    $message = filterText(trim((string)($body['message'] ?? '你好，想认识一下')));
    $dailyLimit = max(1, (int)configValue('friend.requests_per_day', 30));
    rateLimit('user:' . $user['id'], 'friend_request', $dailyLimit, 86400);
    if ($sourceGroupId && strtolower((string)configValue('friend.group_add_requires_vip', 'true')) === 'true') {
        $vip = (int)$user['is_vip'] === 1 && ($user['vip_expires_at'] === null || strtotime($user['vip_expires_at']) > time());
        if (!$vip) fail('群聊内添加好友需要开通 VIP', 403, 'vip_required');
    }
    $stmt = db()->prepare('SELECT id FROM users WHERE public_id=? AND account_status=1');
    $stmt->execute([$targetPublicId]);
    $targetId = (int)$stmt->fetchColumn();
    if (!$targetId || $targetId === (int)$user['id']) fail('目标用户不存在');
    $stmt = db()->prepare('SELECT id,status FROM friendships WHERE (requester_id=? AND addressee_id=?) OR (requester_id=? AND addressee_id=?) ORDER BY id DESC LIMIT 1');
    $stmt->execute([$user['id'], $targetId, $targetId, $user['id']]);
    $existingFriend = $stmt->fetch();
    if ($existingFriend && (int)$existingFriend['status'] === 2) responseJson(['requested' => false, 'already_friends' => true], 200, '你们已经是好友');
    if ($existingFriend && (int)$existingFriend['status'] === 1) responseJson(['requested' => true, 'duplicate' => true], 200, '好友申请正在等待处理');
    $stmt = db()->prepare('INSERT INTO friendships(requester_id,addressee_id,status,source_group_id,message,created_at,updated_at) VALUES(?,?,1,?,?,NOW(),NOW()) ON DUPLICATE KEY UPDATE status=1,message=VALUES(message),updated_at=NOW()');
    $stmt->execute([$user['id'], $targetId, $sourceGroupId, $message]);
    responseJson(['requested' => true], 201, '好友申请已发送');
}

if ($method === 'POST' && preg_match('#^/v1/friends/requests/(\d+)/(accept|reject)$#', $path, $match)) {
    $user = requireUser();
    $status = $match[2] === 'accept' ? 2 : 3;
    $stmt = db()->prepare('UPDATE friendships SET status=?,updated_at=NOW() WHERE id=? AND addressee_id=? AND status=1');
    $stmt->execute([$status, (int)$match[1], $user['id']]);
    if ($stmt->rowCount() !== 1) fail('好友申请不存在或已处理', 404);
    responseJson(['status' => $status], 200, $status === 2 ? '已成为好友' : '已拒绝');
}

if ($method === 'GET' && preg_match('#^/v1/users/([A-Z0-9]+)$#', $path, $match)) {
    requireUser();
    $stmt = db()->prepare('SELECT public_id,nickname,avatar_url,gender,city,bio,realname_status,is_vip,vip_expires_at,created_at FROM users WHERE public_id=? AND account_status=1 AND deleted_at IS NULL');
    $stmt->execute([$match[1]]); $profile = $stmt->fetch();
    if (!$profile) fail('用户不存在', 404);
    responseJson($profile);
}

if ($method === 'GET' && preg_match('#^/v1/direct/([A-Z0-9]+)/messages$#', $path, $match)) {
    $user = requireUser();
    $stmt = db()->prepare('SELECT id,public_id,nickname,avatar_url,is_vip FROM users WHERE public_id=? AND account_status=1'); $stmt->execute([$match[1]]); $target = $stmt->fetch();
    if (!$target || (int)$target['id'] === (int)$user['id']) fail('聊天对象不存在', 404);
    $stmt = db()->prepare('SELECT id FROM friendships WHERE status=2 AND ((requester_id=? AND addressee_id=?) OR (requester_id=? AND addressee_id=?)) LIMIT 1');
    $stmt->execute([$user['id'], $target['id'], $target['id'], $user['id']]); if (!$stmt->fetch()) fail('成为好友后才能私聊', 403);
    $conversationId = directConversationId((int)$user['id'], (int)$target['id']);
    $stmt = db()->prepare('SELECT m.message_no,m.sender_user_id,m.content,m.message_type,m.created_at,u.public_id,u.nickname,u.avatar_url,u.is_vip FROM chat_messages m JOIN users u ON u.id=m.sender_user_id WHERE m.conversation_type=1 AND m.conversation_id=? AND m.deleted_at IS NULL ORDER BY m.id DESC LIMIT 100');
    $stmt->execute([$conversationId]);
    responseJson(['target' => $target, 'items' => array_reverse($stmt->fetchAll())]);
}

if ($method === 'POST' && preg_match('#^/v1/direct/([A-Z0-9]+)/messages$#', $path, $match)) {
    $user = requireUser();
    $stmt = db()->prepare('SELECT id FROM users WHERE public_id=? AND account_status=1'); $stmt->execute([$match[1]]); $targetId = (int)$stmt->fetchColumn();
    if (!$targetId || $targetId === (int)$user['id']) fail('聊天对象不存在', 404);
    $stmt = db()->prepare('SELECT id FROM friendships WHERE status=2 AND ((requester_id=? AND addressee_id=?) OR (requester_id=? AND addressee_id=?)) LIMIT 1');
    $stmt->execute([$user['id'], $targetId, $targetId, $user['id']]); if (!$stmt->fetch()) fail('成为好友后才能私聊', 403);
    rateLimit('user:' . $user['id'], 'direct_message', max(1, (int)configValue('chat.messages_per_minute', 20)), 60);
    $content = trim((string)(requestBody()['content'] ?? '')); $maxChars = max(10, (int)configValue('chat.message_max_chars', 500));
    if ($content === '' || mb_strlen($content, 'UTF-8') > $maxChars) fail('消息为空或超过字数限制');
    $content = filterText($content); $conversationId = directConversationId((int)$user['id'], $targetId); $messageNo = randomCode('QM', 8);
    db()->prepare('INSERT INTO chat_messages(message_no,conversation_type,conversation_id,sender_user_id,message_type,content,review_status,created_at) VALUES(?,1,?,? ,"text",?,1,NOW())')->execute([$messageNo, $conversationId, $user['id'], $content]);
    db()->prepare('UPDATE direct_conversations SET last_message_at=NOW() WHERE id=?')->execute([$conversationId]);
    responseJson(['message_no' => $messageNo, 'content' => $content], 201, '发送成功');
}

if ($method === 'GET' && $path === '/v1/moments') {
    $stmt = db()->query('SELECT m.public_id,m.content,m.media_urls,m.like_count,m.comment_count,m.created_at,u.public_id AS user_public_id,u.nickname,u.avatar_url,u.is_vip FROM moments m JOIN users u ON u.id=m.user_id WHERE m.review_status=2 AND m.deleted_at IS NULL ORDER BY m.id DESC LIMIT 100');
    responseJson(['items' => $stmt->fetchAll()]);
}

if ($method === 'POST' && $path === '/v1/moments') {
    $user = requireUser();
    rateLimit('user:' . $user['id'], 'create_moment', 20, 86400);
    $body = requestBody();
    $content = filterText(trim((string)($body['content'] ?? '')));
    if ($content === '' || mb_strlen($content, 'UTF-8') > 2000) fail('动态内容为空或超过2000字');
    $mediaUrls = trim((string)($body['media_urls'] ?? ''));
    $publicId = randomCode('D', 8);
    db()->prepare('INSERT INTO moments(public_id,user_id,content,media_urls,review_status,created_at,updated_at) VALUES(?,?,?,?,2,NOW(),NOW())')->execute([$publicId, $user['id'], $content, $mediaUrls]);
    responseJson(['public_id' => $publicId, 'content' => $content], 201, '动态已发布');
}

if ($method === 'POST' && $path === '/v1/reports') {
    $user = requireUser();
    $body = requestBody();
    $targetType = (string)($body['target_type'] ?? '');
    $targetId = (string)($body['target_id'] ?? '');
    $reason = filterText(trim((string)($body['reason'] ?? '')));
    if (!in_array($targetType, ['user','group','message','moment'], true) || $targetId === '' || mb_strlen($reason, 'UTF-8') < 3) fail('举报信息不完整');
    rateLimit('user:' . $user['id'], 'report', 30, 86400);
    db()->prepare('INSERT INTO reports(reporter_user_id,target_type,target_id,reason,status,created_at) VALUES(?,?,?,?,1,NOW())')->execute([$user['id'], $targetType, $targetId, $reason]);
    responseJson(['submitted' => true], 201, '举报已提交');
}

if ($method === 'GET' && $path === '/v1/membership/plans') {
    responseJson(['items' => db()->query('SELECT id,plan_code,name,billing_period,price_cents,benefits FROM membership_plans WHERE enabled=1 ORDER BY sort_order')->fetchAll()]);
}

if ($method === 'POST' && $path === '/v1/membership/orders') {
    $user = requireUser(); $body = requestBody(); $planId = (int)($body['plan_id'] ?? 0); $channel = (string)($body['channel'] ?? 'wechat_app');
    if (!in_array($channel, ['wechat_app','alipay_app'], true)) fail('支付渠道不支持');
    $stmt = db()->prepare('SELECT * FROM membership_plans WHERE id=? AND enabled=1'); $stmt->execute([$planId]); $plan = $stmt->fetch();
    if (!$plan) fail('会员套餐不存在', 404);
    $orderNo = randomCode('QV', 8); $paymentNo = randomCode('QVP', 8); $pdo = db(); $pdo->beginTransaction();
    try {
        $stmt = $pdo->prepare('INSERT INTO membership_orders(order_no,user_id,plan_id,amount_cents,status,created_at,updated_at) VALUES(?,?,?,?,1,NOW(),NOW())'); $stmt->execute([$orderNo, $user['id'], $planId, $plan['price_cents']]);
        $orderId = (int)$pdo->lastInsertId();
        $pdo->prepare('INSERT INTO membership_payments(membership_order_id,payment_no,channel,amount_cents,status,created_at,updated_at) VALUES(?,?,?,?,1,NOW(),NOW())')->execute([$orderId, $paymentNo, $channel, $plan['price_cents']]);
        $pdo->commit();
    } catch (Throwable $error) { if ($pdo->inTransaction()) $pdo->rollBack(); throw $error; }
    $stmt = db()->prepare('SELECT public_config,enabled,environment FROM provider_configs WHERE provider_type="payment" AND provider_code=?'); $stmt->execute([$channel]); $provider = $stmt->fetch() ?: ['public_config'=>'{}','enabled'=>0,'environment'=>'sandbox'];
    responseJson(['order_no'=>$orderNo,'payment_no'=>$paymentNo,'amount_cents'=>(int)$plan['price_cents'],'channel'=>$channel,'provider_enabled'=>(bool)$provider['enabled'],'provider_environment'=>$provider['environment'],'provider_config'=>json_decode((string)$provider['public_config'],true) ?: []], 201, '会员订单已创建');
}

if ($method === 'POST' && $path === '/v1/admin/login') {
    $body = requestBody();
    $username = trim((string)($body['username'] ?? ''));
    $password = (string)($body['password'] ?? '');
    rateLimit(clientIp(), 'admin_login', 20, 600);
    $count = (int)db()->query('SELECT COUNT(*) FROM admin_users')->fetchColumn();
    if ($count === 0) {
        $initialUser = (string)appConfig('security.initial_admin_username');
        $initialPassword = (string)appConfig('security.initial_admin_password');
        if (startsWith($initialPassword, 'CHANGE_ME')) fail('请先在 config.php 设置首次管理员密码', 503);
        if (hash_equals($initialUser, $username) && hash_equals($initialPassword, $password)) {
            $stmt = db()->prepare('INSERT INTO admin_users(username,password_hash,display_name,role_code,status,created_at,updated_at) VALUES(?,?,"超级管理员","super_admin",1,NOW(),NOW())');
            $stmt->execute([$username, password_hash($password, PASSWORD_DEFAULT)]);
        }
    }
    $stmt = db()->prepare('SELECT * FROM admin_users WHERE username=? AND status=1 LIMIT 1');
    $stmt->execute([$username]);
    $admin = $stmt->fetch();
    if (!$admin || !password_verify($password, $admin['password_hash'])) fail('后台账号或密码不正确', 401);
    db()->prepare('UPDATE admin_users SET last_login_at=NOW(),updated_at=NOW() WHERE id=?')->execute([$admin['id']]);
    $token = issueAdminSession((int)$admin['id']);
    responseJson(['token' => $token, 'admin' => ['username' => $admin['username'], 'display_name' => $admin['display_name'], 'role_code' => $admin['role_code']]], 200, '登录成功');
}

if ($method === 'GET' && $path === '/v1/admin/dashboard') {
    $admin = requireAdmin();
    releaseDueFunds();
    $metrics = [
        'users_total' => (int)db()->query('SELECT COUNT(*) FROM users WHERE id<>1 AND deleted_at IS NULL')->fetchColumn(),
        'users_today' => (int)db()->query('SELECT COUNT(*) FROM users WHERE id<>1 AND created_at>=CURDATE()')->fetchColumn(),
        'groups_online' => (int)db()->query('SELECT COUNT(*) FROM chat_groups WHERE review_status=3 AND deleted_at IS NULL')->fetchColumn(),
        'orders_total' => (int)db()->query('SELECT COUNT(*) FROM orders')->fetchColumn(),
        'orders_paid_cents' => (int)db()->query('SELECT COALESCE(SUM(amount_cents),0) FROM orders WHERE status IN (2,3,4,5)')->fetchColumn(),
        'refunds_pending' => (int)db()->query('SELECT COUNT(*) FROM refunds WHERE status IN (1,2,5)')->fetchColumn(),
        'withdrawals_pending' => (int)db()->query('SELECT COUNT(*) FROM withdrawal_requests WHERE status IN (1,2,3)')->fetchColumn(),
        'reports_pending' => (int)db()->query('SELECT COUNT(*) FROM reports WHERE status IN (1,2)')->fetchColumn(),
    ];
    foreach ([1,3,7,30,90,180,360,720] as $retentionDay) {
        $cohortDate = date('Y-m-d', strtotime('-' . $retentionDay . ' days'));
        $stmt = db()->prepare('SELECT COUNT(*) AS cohort_count, SUM(IF(EXISTS(SELECT 1 FROM user_daily_activity a WHERE a.user_id=u.id AND a.activity_date=CURDATE()),1,0)) AS retained_count FROM users u WHERE DATE(u.created_at)=? AND u.id<>1');
        $stmt->execute([$cohortDate]);
        $retention = $stmt->fetch();
        $cohortCount = (int)($retention['cohort_count'] ?? 0);
        $metrics['retention_d' . $retentionDay] = $cohortCount > 0 ? round((int)$retention['retained_count'] * 100 / $cohortCount, 2) : null;
    }
    responseJson(['admin' => ['display_name' => $admin['display_name']], 'metrics' => $metrics]);
}

if ($method === 'GET' && $path === '/v1/admin/users') {
    requireAdmin();
    $keyword = trim((string)($_GET['keyword'] ?? ''));
    $sql = 'SELECT id,public_id,nickname,gender,city,bio,account_status,realname_status,is_vip,vip_expires_at,last_login_at,created_at,phone_ciphertext FROM users WHERE id<>1';
    $params = [];
    if ($keyword !== '') { $sql .= ' AND (public_id LIKE ? OR nickname LIKE ?)'; $like = '%' . $keyword . '%'; $params = [$like, $like]; }
    $sql .= ' ORDER BY id DESC LIMIT 200';
    $stmt = db()->prepare($sql); $stmt->execute($params);
    $items = $stmt->fetchAll();
    foreach ($items as &$item) {
        $phone = decryptValue($item['phone_ciphertext']);
        $item['phone_masked'] = strlen($phone) === 11 ? substr($phone,0,3) . '****' . substr($phone,-4) : '***';
        unset($item['phone_ciphertext']);
    }
    responseJson(['items' => $items]);
}

if ($method === 'PATCH' && preg_match('#^/v1/admin/users/(\d+)$#', $path, $match)) {
    $admin = requireAdmin();
    $body = requestBody();
    $status = (int)($body['account_status'] ?? 1);
    if (!in_array($status, [1,2,3,4,5], true)) fail('账号状态不正确');
    $userId = (int)$match[1];
    $stmt = db()->prepare('SELECT * FROM users WHERE id=? AND id<>1'); $stmt->execute([$userId]); $current = $stmt->fetch();
    if (!$current) fail('用户不存在', 404);
    $nickname = filterText(trim((string)($body['nickname'] ?? $current['nickname'])));
    $city = trim((string)($body['city'] ?? $current['city'] ?? ''));
    $bio = filterText(trim((string)($body['bio'] ?? $current['bio'] ?? '')));
    $gender = (int)($body['gender'] ?? $current['gender']);
    $realnameStatus = (int)($body['realname_status'] ?? $current['realname_status']);
    $isVip = array_key_exists('is_vip', $body) ? (!empty($body['is_vip']) ? 1 : 0) : (int)$current['is_vip'];
    $vipExpiresAt = $isVip ? (array_key_exists('vip_expires_at', $body) ? (trim((string)$body['vip_expires_at']) ?: null) : $current['vip_expires_at']) : null;
    if (mb_strlen($nickname, 'UTF-8') < 2 || mb_strlen($nickname, 'UTF-8') > 20 || mb_strlen($bio, 'UTF-8') > 160) fail('用户资料长度不正确');
    if (!in_array($gender, [0,1,2,3], true) || !in_array($realnameStatus, [0,1,2,3], true)) fail('用户资料状态不正确');
    db()->prepare('UPDATE users SET nickname=?,gender=?,city=?,bio=?,account_status=?,realname_status=?,is_vip=?,vip_expires_at=?,updated_at=NOW() WHERE id=? AND id<>1')->execute([$nickname,$gender,$city,$bio,$status,$realnameStatus,$isVip,$vipExpiresAt,$userId]);
    if ($status !== 1) db()->prepare('UPDATE user_sessions SET revoked_at=NOW() WHERE user_id=? AND revoked_at IS NULL')->execute([$userId]);
    audit($admin, 'users', 'update_profile', 'user', $match[1], ['account_status'=>$status,'realname_status'=>$realnameStatus,'is_vip'=>$isVip]);
    responseJson(['updated' => true], 200, '用户资料已更新');
}

if ($method === 'GET' && $path === '/v1/admin/orders') {
    requireAdmin();
    $stmt = db()->query('SELECT o.order_no,o.title,o.amount_cents,o.platform_fee_cents,o.owner_income_cents,o.status,o.paid_at,o.created_at,u.public_id,u.nickname,g.name AS group_name FROM orders o JOIN users u ON u.id=o.user_id JOIN chat_groups g ON g.id=o.group_id ORDER BY o.id DESC LIMIT 300');
    responseJson(['items' => $stmt->fetchAll()]);
}

if ($method === 'GET' && $path === '/v1/admin/categories') {
    requireAdmin();
    responseJson(['items' => db()->query('SELECT * FROM group_categories ORDER BY sort_order,id')->fetchAll()]);
}

if ($method === 'POST' && $path === '/v1/admin/categories') {
    $admin = requireAdmin();
    $body = requestBody();
    $code = preg_replace('/[^a-z0-9_]/', '', strtolower(trim((string)($body['code'] ?? ''))));
    $name = trim((string)($body['name'] ?? ''));
    if ($code === '' || $name === '' || mb_strlen($name, 'UTF-8') > 32) fail('分类编码或名称不正确');
    $stmt = db()->prepare('INSERT INTO group_categories(code,name,description,icon,color_hex,sort_order,enabled,created_at,updated_at) VALUES(?,?,?,?,?,?,?,NOW(),NOW())');
    $stmt->execute([$code, $name, trim((string)($body['description'] ?? '')), trim((string)($body['icon'] ?? '👥')), trim((string)($body['color_hex'] ?? '#6C55E6')), (int)($body['sort_order'] ?? 0), !empty($body['enabled']) ? 1 : 0]);
    audit($admin, 'groups', 'create_category', 'category', (string)db()->lastInsertId(), ['name' => $name]);
    responseJson(['created' => true], 201, '分类已创建');
}

if ($method === 'PUT' && preg_match('#^/v1/admin/categories/(\d+)$#', $path, $match)) {
    $admin = requireAdmin();
    $body = requestBody();
    $code = preg_replace('/[^a-z0-9_]/', '', strtolower(trim((string)($body['code'] ?? ''))));
    $name = trim((string)($body['name'] ?? ''));
    if ($code === '' || $name === '') fail('分类编码或名称不正确');
    $stmt = db()->prepare('UPDATE group_categories SET code=?,name=?,description=?,icon=?,color_hex=?,sort_order=?,enabled=?,updated_at=NOW() WHERE id=?');
    $stmt->execute([$code, $name, trim((string)($body['description'] ?? '')), trim((string)($body['icon'] ?? '👥')), trim((string)($body['color_hex'] ?? '#6C55E6')), (int)($body['sort_order'] ?? 0), !empty($body['enabled']) ? 1 : 0, (int)$match[1]]);
    audit($admin, 'groups', 'update_category', 'category', $match[1], ['name' => $name]);
    responseJson(['updated' => true], 200, '分类已更新');
}

if ($method === 'GET' && $path === '/v1/admin/groups') {
    requireAdmin();
    $stmt = db()->query('SELECT g.*,c.name AS category_name,u.nickname AS owner_name FROM chat_groups g JOIN group_categories c ON c.id=g.category_id JOIN users u ON u.id=g.owner_user_id WHERE g.deleted_at IS NULL ORDER BY g.id DESC LIMIT 300');
    responseJson(['items' => $stmt->fetchAll()]);
}

if ($method === 'POST' && $path === '/v1/admin/groups') {
    $admin = requireAdmin();
    $body = requestBody();
    $name = filterText(trim((string)($body['name'] ?? '')));
    $summary = filterText(trim((string)($body['summary'] ?? '')));
    $joinType = (int)($body['join_type'] ?? 1);
    $price = max(0, (int)($body['price_cents'] ?? 0));
    if ($name === '' || $summary === '' || !in_array($joinType, [1,2,3], true)) fail('群聊资料不完整');
    if ($joinType === 2 && $price < 1) fail('付费群价格必须大于0');
    $publicId = randomCode('G', 6);
    $pdo = db(); $pdo->beginTransaction();
    try {
        $stmt = $pdo->prepare('INSERT INTO chat_groups(public_id,owner_user_id,category_id,name,summary,description,city,join_type,price_cents,requires_review,member_limit,member_count,review_status,verified_badge,created_at,updated_at) VALUES(?,1,?,?,?,?,?,?,?,?,?,1,3,1,NOW(),NOW())');
        $stmt->execute([$publicId, (int)($body['category_id'] ?? 1), $name, $summary, filterText(trim((string)($body['description'] ?? ''))), trim((string)($body['city'] ?? '全国')), $joinType, $joinType === 2 ? $price : 0, !empty($body['requires_review']) ? 1 : 0, max(2, (int)($body['member_limit'] ?? 500))]);
        $groupId = (int)$pdo->lastInsertId();
        $pdo->prepare('INSERT INTO group_members(group_id,user_id,role,status,joined_at) VALUES(?,1,3,1,NOW())')->execute([$groupId]);
        $pdo->commit();
        audit($admin, 'groups', 'create_group', 'group', (string)$groupId, ['name' => $name]);
        responseJson(['id' => $groupId, 'public_id' => $publicId], 201, '群聊已创建');
    } catch (Throwable $error) { if ($pdo->inTransaction()) $pdo->rollBack(); throw $error; }
}

if ($method === 'PUT' && preg_match('#^/v1/admin/groups/(\d+)$#', $path, $match)) {
    $admin = requireAdmin();
    $body = requestBody();
    $joinType = (int)($body['join_type'] ?? 1);
    $price = max(0, (int)($body['price_cents'] ?? 0));
    if (!in_array($joinType, [1,2,3], true) || ($joinType === 2 && $price < 1)) fail('进群方式或价格不正确');
    $stmt = db()->prepare('UPDATE chat_groups SET category_id=?,name=?,summary=?,description=?,city=?,join_type=?,price_cents=?,requires_review=?,member_limit=?,updated_at=NOW() WHERE id=? AND deleted_at IS NULL');
    $stmt->execute([(int)($body['category_id'] ?? 1), filterText(trim((string)($body['name'] ?? ''))), filterText(trim((string)($body['summary'] ?? ''))), filterText(trim((string)($body['description'] ?? ''))), trim((string)($body['city'] ?? '全国')), $joinType, $joinType === 2 ? $price : 0, !empty($body['requires_review']) ? 1 : 0, max(2, (int)($body['member_limit'] ?? 500)), (int)$match[1]]);
    audit($admin, 'groups', 'update_group', 'group', $match[1]);
    responseJson(['updated' => true], 200, '群聊已更新');
}

if ($method === 'PATCH' && preg_match('#^/v1/admin/groups/(\d+)/status$#', $path, $match)) {
    $admin = requireAdmin();
    $status = (int)(requestBody()['review_status'] ?? 0);
    if (!in_array($status, [2,3,4,5], true)) fail('群聊状态不正确');
    db()->prepare('UPDATE chat_groups SET review_status=?,updated_at=NOW() WHERE id=? AND deleted_at IS NULL')->execute([$status, (int)$match[1]]);
    audit($admin, 'groups', 'update_status', 'group', $match[1], ['review_status' => $status]);
    responseJson(['status' => $status], 200, '群聊状态已更新');
}

if ($method === 'GET' && preg_match('#^/v1/admin/groups/(\d+)/members$#', $path, $match)) {
    requireAdmin();
    $stmt = db()->prepare('SELECT u.id AS user_id,u.public_id,u.nickname,u.avatar_url,u.account_status,m.role,m.status,m.mute_until,m.joined_at,m.left_at FROM group_members m JOIN users u ON u.id=m.user_id WHERE m.group_id=? ORDER BY m.role DESC,m.status ASC,m.id ASC LIMIT 2000');
    $stmt->execute([(int)$match[1]]);
    responseJson(['items' => $stmt->fetchAll()]);
}

if ($method === 'PATCH' && preg_match('#^/v1/admin/groups/(\d+)/members/(\d+)$#', $path, $match)) {
    $admin = requireAdmin();
    $groupId = (int)$match[1];
    $userId = (int)$match[2];
    $body = requestBody();
    $status = (int)($body['status'] ?? 1);
    $muteHours = max(1, min(8760, (int)($body['mute_hours'] ?? 24)));
    if (!in_array($status, [1,2,3], true)) fail('成员状态不正确');
    $stmt = db()->prepare('SELECT id,role FROM group_members WHERE group_id=? AND user_id=? LIMIT 1');
    $stmt->execute([$groupId, $userId]);
    $member = $stmt->fetch();
    if (!$member) fail('群成员不存在', 404);
    if ((int)$member['role'] === 3 && $status === 3) fail('不能从群中移出群主', 409);
    if ($status === 2) {
        db()->prepare('UPDATE group_members SET status=2,mute_until=DATE_ADD(NOW(),INTERVAL ? HOUR),left_at=NULL WHERE id=?')->execute([$muteHours, $member['id']]);
    } elseif ($status === 3) {
        db()->prepare('UPDATE group_members SET status=3,mute_until=NULL,left_at=NOW() WHERE id=?')->execute([$member['id']]);
    } else {
        db()->prepare('UPDATE group_members SET status=1,mute_until=NULL,left_at=NULL WHERE id=?')->execute([$member['id']]);
    }
    db()->prepare('UPDATE chat_groups SET member_count=(SELECT COUNT(*) FROM group_members WHERE group_id=? AND status IN (1,2)),updated_at=NOW() WHERE id=?')->execute([$groupId, $groupId]);
    audit($admin, 'groups', 'update_member_status', 'group_member', (string)$member['id'], ['group_id' => $groupId, 'user_id' => $userId, 'status' => $status, 'mute_hours' => $status === 2 ? $muteHours : 0]);
    responseJson(['status' => $status, 'mute_hours' => $status === 2 ? $muteHours : 0], 200, '群成员状态已更新');
}

if ($method === 'GET' && $path === '/v1/admin/refunds') {
    requireAdmin();
    $stmt = db()->query('SELECT r.*,o.order_no,o.title,u.public_id,u.nickname FROM refunds r JOIN orders o ON o.id=r.order_id JOIN users u ON u.id=r.applicant_user_id ORDER BY r.id DESC LIMIT 200');
    responseJson(['items' => $stmt->fetchAll()]);
}

if ($method === 'POST' && preg_match('#^/v1/admin/refunds/(\d+)/(approve|reject|complete)$#', $path, $match)) {
    $admin = requireAdmin();
    $body = requestBody();
    $note = trim((string)($body['note'] ?? ''));
    $action = $match[2];
    $status = $action === 'approve' ? 5 : ($action === 'reject' ? 4 : 6);
    $pdo = db();
    $pdo->beginTransaction();
    try {
        $stmt = $pdo->prepare('SELECT r.*,o.order_no,o.owner_user_id,o.id AS order_id FROM refunds r JOIN orders o ON o.id=r.order_id WHERE r.id=? FOR UPDATE');
        $stmt->execute([(int)$match[1]]);
        $refund = $stmt->fetch();
        if (!$refund) fail('退款单不存在', 404);
        $refundStatus = (int)$refund['status'];
        if ($action === 'approve' && !in_array($refundStatus, [1,2], true)) fail('退款单当前状态不能通过', 409);
        if ($action === 'reject' && !in_array($refundStatus, [1,2], true)) fail('退款单当前状态不能驳回', 409);
        if ($action === 'complete' && $refundStatus !== 5) fail('只有退款中的订单才能确认完成', 409);
        if ($action === 'complete') {
            $stmt = $pdo->prepare('SELECT p.*,w.id AS wallet_id FROM wallet_pending_funds p JOIN wallet_accounts w ON w.id=p.account_id WHERE p.order_id=? FOR UPDATE');
            $stmt->execute([$refund['order_id']]);
            $fund = $stmt->fetch();
            if ($fund) {
                $remaining = max(0, (int)$fund['pending_cents'] - (int)$fund['refunded_cents']);
                $deduct = min($remaining, (int)$refund['amount_cents']);
                if ((int)$fund['status'] === 1) db()->prepare('UPDATE wallet_accounts SET pending_cents=pending_cents-?,updated_at=NOW() WHERE id=?')->execute([$deduct, $fund['wallet_id']]);
                else db()->prepare('UPDATE wallet_accounts SET available_cents=available_cents-?,updated_at=NOW() WHERE id=?')->execute([$deduct, $fund['wallet_id']]);
                db()->prepare('UPDATE wallet_pending_funds SET refunded_cents=refunded_cents+?,status=4,updated_at=NOW() WHERE id=?')->execute([$deduct, $fund['id']]);
                $balanceField = (int)$fund['status'] === 1 ? 'pending_cents' : 'available_cents';
                $balanceStmt = $pdo->prepare('SELECT ' . $balanceField . ' FROM wallet_accounts WHERE id=?'); $balanceStmt->execute([$fund['wallet_id']]);
                $pdo->prepare('INSERT IGNORE INTO wallet_ledger(account_id,biz_type,biz_no,direction,amount_cents,balance_after_cents,remark,created_at) VALUES(?,"refund_deduct",?,2,?,?,"进群订单退款扣减",NOW())')->execute([$fund['wallet_id'], $refund['order_no'], $deduct, (int)$balanceStmt->fetchColumn()]);
            }
            db()->prepare('UPDATE orders SET status=5,updated_at=NOW() WHERE id=?')->execute([$refund['order_id']]);
            db()->prepare('UPDATE group_members gm JOIN orders o ON o.group_id=gm.group_id AND o.user_id=gm.user_id SET gm.status=3,gm.left_at=NOW() WHERE o.id=?')->execute([$refund['order_id']]);
        }
        $stmt = $pdo->prepare('UPDATE refunds SET status=?,review_note=?,reviewer_admin_id=?,reviewed_at=NOW(),refunded_at=IF(?=6,NOW(),refunded_at) WHERE id=?');
        $stmt->execute([$status, $note, $admin['id'], $status, (int)$match[1]]);
        if ($action === 'reject') $pdo->prepare('UPDATE orders SET status=2,updated_at=NOW() WHERE id=?')->execute([$refund['order_id']]);
        $pdo->commit();
        audit($admin, 'refunds', $action, 'refund', $match[1], ['note' => $note]);
        responseJson(['status' => $status], 200, '退款状态已更新');
    } catch (Throwable $error) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        throw $error;
    }
}

if ($method === 'GET' && $path === '/v1/admin/withdrawals') {
    requireAdmin();
    $stmt = db()->query('SELECT w.*,u.public_id,u.nickname FROM withdrawal_requests w JOIN users u ON u.id=w.user_id ORDER BY w.id DESC LIMIT 200');
    $items = $stmt->fetchAll();
    foreach ($items as &$item) {
        $accountNo = decryptValue((string)$item['account_no']);
        $item['account_no_masked'] = mb_strlen($accountNo, 'UTF-8') > 4 ? str_repeat('*', max(4, mb_strlen($accountNo, 'UTF-8') - 4)) . mb_substr($accountNo, -4, 4, 'UTF-8') : '****';
        unset($item['account_no']);
    }
    responseJson(['items' => $items]);
}

if ($method === 'POST' && preg_match('#^/v1/admin/withdrawals/(\d+)/(approve|reject)$#', $path, $match)) {
    $admin = requireAdmin();
    $note = trim((string)(requestBody()['note'] ?? ''));
    $pdo = db(); $pdo->beginTransaction();
    try {
        $stmt = $pdo->prepare('SELECT w.*,a.id AS wallet_id FROM withdrawal_requests w JOIN wallet_accounts a ON a.user_id=w.user_id WHERE w.id=? FOR UPDATE');
        $stmt->execute([(int)$match[1]]); $withdrawal = $stmt->fetch();
        if (!$withdrawal) fail('提现单不存在', 404);
        $oldStatus = (int)$withdrawal['status'];
        if ($oldStatus >= 4) fail('提现单已经结束处理', 409);
        if ($match[2] === 'reject') {
            $newStatus = 5;
            $pdo->prepare('UPDATE wallet_accounts SET frozen_cents=GREATEST(0,frozen_cents-?),available_cents=available_cents+?,updated_at=NOW() WHERE id=?')->execute([$withdrawal['amount_cents'], $withdrawal['amount_cents'], $withdrawal['wallet_id']]);
        } else {
            $newStatus = min(4, $oldStatus + 1);
            if ($newStatus === 4) $pdo->prepare('UPDATE wallet_accounts SET frozen_cents=GREATEST(0,frozen_cents-?),updated_at=NOW() WHERE id=?')->execute([$withdrawal['amount_cents'], $withdrawal['wallet_id']]);
        }
        if ($match[2] === 'reject' || $newStatus === 4) {
            $balanceStmt = $pdo->prepare('SELECT available_cents FROM wallet_accounts WHERE id=?'); $balanceStmt->execute([$withdrawal['wallet_id']]);
            $pdo->prepare('INSERT IGNORE INTO wallet_ledger(account_id,biz_type,biz_no,direction,amount_cents,balance_after_cents,remark,created_at) VALUES(?,?,?,?,?,?,?,NOW())')->execute([$withdrawal['wallet_id'], $match[2] === 'reject' ? 'withdraw_unfreeze' : 'withdraw_complete', $withdrawal['withdrawal_no'], $match[2] === 'reject' ? 4 : 2, $withdrawal['amount_cents'], (int)$balanceStmt->fetchColumn(), $match[2] === 'reject' ? '提现拒绝退回' : '提现打款完成']);
        }
        $stmt = $pdo->prepare('UPDATE withdrawal_requests SET status=?,review_note=?,reviewed_at=IF(? >= 2,NOW(),reviewed_at),completed_at=IF(?=4,NOW(),completed_at) WHERE id=?');
        $stmt->execute([$newStatus, $note, $newStatus, $newStatus, (int)$match[1]]);
        $pdo->commit();
        audit($admin, 'withdrawals', $match[2], 'withdrawal', $match[1], ['status' => $newStatus, 'note' => $note]);
        responseJson(['status' => $newStatus], 200, $newStatus === 4 ? '提现已完成' : '提现状态已更新');
    } catch (Throwable $error) { if ($pdo->inTransaction()) $pdo->rollBack(); throw $error; }
}

if ($method === 'GET' && $path === '/v1/admin/reports') {
    requireAdmin();
    $stmt = db()->query('SELECT r.*,u.public_id,u.nickname FROM reports r JOIN users u ON u.id=r.reporter_user_id ORDER BY r.id DESC LIMIT 300');
    responseJson(['items' => $stmt->fetchAll()]);
}

if ($method === 'POST' && preg_match('#^/v1/admin/reports/(\d+)/(confirm|reject)$#', $path, $match)) {
    $admin = requireAdmin();
    $status = $match[2] === 'confirm' ? 3 : 4;
    db()->prepare('UPDATE reports SET status=?,handled_at=NOW() WHERE id=? AND status IN (1,2)')->execute([$status, (int)$match[1]]);
    audit($admin, 'reports', $match[2], 'report', $match[1]);
    responseJson(['status' => $status], 200, '举报已处理');
}

if ($method === 'GET' && $path === '/v1/admin/configs') {
    requireAdmin();
    $items = db()->query('SELECT id,config_key,IF(is_secret=1,"已配置/受保护",config_value) AS config_value,value_type,description,is_secret,updated_at FROM system_configs ORDER BY id')->fetchAll();
    responseJson(['items' => $items]);
}

if ($method === 'PUT' && $path === '/v1/admin/configs') {
    $admin = requireAdmin();
    $body = requestBody();
    $items = $body['items'] ?? [];
    if (!is_array($items) || count($items) > 200) fail('配置数据不正确');
    $stmt = db()->prepare('UPDATE system_configs SET config_value=?,updated_at=NOW() WHERE config_key=? AND is_secret=0');
    foreach ($items as $key => $value) $stmt->execute([(string)$value, (string)$key]);
    if (array_key_exists('membership.vip_month_cents', $items)) db()->prepare('UPDATE membership_plans SET price_cents=?,updated_at=NOW() WHERE plan_code="vip_month"')->execute([(int)$items['membership.vip_month_cents']]);
    if (array_key_exists('membership.vip_year_cents', $items)) db()->prepare('UPDATE membership_plans SET price_cents=?,updated_at=NOW() WHERE plan_code="vip_year"')->execute([(int)$items['membership.vip_year_cents']]);
    audit($admin, 'configs', 'batch_update', 'system_config', null, ['keys' => array_keys($items)]);
    responseJson(['updated' => count($items)], 200, '配置已保存');
}

if ($method === 'GET' && $path === '/v1/admin/providers') {
    requireAdmin();
    $items = db()->query('SELECT id,provider_type,provider_code,display_name,public_config,IF(secret_config IS NULL OR secret_config="",0,1) AS secret_configured,environment,enabled,health_status,updated_at FROM provider_configs ORDER BY provider_type,id')->fetchAll();
    responseJson(['items' => $items]);
}

if ($method === 'PUT' && preg_match('#^/v1/admin/providers/(\d+)$#', $path, $match)) {
    $admin = requireAdmin();
    $body = requestBody();
    $public = $body['public_config'] ?? [];
    $secret = $body['secret_config'] ?? null;
    $environment = in_array(($body['environment'] ?? ''), ['sandbox','production'], true) ? $body['environment'] : 'sandbox';
    $enabled = !empty($body['enabled']) ? 1 : 0;
    if ($environment === 'production' && $secret === null) {
        $check = db()->prepare('SELECT secret_config FROM provider_configs WHERE id=?'); $check->execute([(int)$match[1]]);
        if (!(string)$check->fetchColumn()) fail('生产环境必须先填写密钥配置');
    }
    $secretEncrypted = $secret === null ? null : encryptValue(json_encode($secret, JSON_UNESCAPED_UNICODE));
    $stmt = db()->prepare('UPDATE provider_configs SET public_config=?,secret_config=COALESCE(?,secret_config),environment=?,enabled=?,updated_at=NOW() WHERE id=?');
    $stmt->execute([json_encode($public, JSON_UNESCAPED_UNICODE), $secretEncrypted, $environment, $enabled, (int)$match[1]]);
    audit($admin, 'providers', 'update', 'provider', $match[1], ['environment' => $environment, 'enabled' => $enabled]);
    responseJson(['updated' => true], 200, '服务商配置已保存');
}

if ($method === 'GET' && $path === '/v1/admin/sensitive-words') {
    requireAdmin();
    responseJson(['items' => db()->query('SELECT * FROM sensitive_words ORDER BY id DESC')->fetchAll()]);
}

if ($method === 'POST' && $path === '/v1/admin/sensitive-words') {
    $admin = requireAdmin();
    $body = requestBody();
    $word = trim((string)($body['word_text'] ?? ''));
    $category = trim((string)($body['category'] ?? '其他'));
    $action = (int)($body['action_type'] ?? 1);
    if ($word === '' || mb_strlen($word, 'UTF-8') > 100 || !in_array($action, [1,2,3], true)) fail('敏感词参数不正确');
    db()->prepare('INSERT INTO sensitive_words(word_text,category,action_type,replacement_char,enabled,created_at,updated_at) VALUES(?,?,?,"*",1,NOW(),NOW()) ON DUPLICATE KEY UPDATE category=VALUES(category),action_type=VALUES(action_type),enabled=1,updated_at=NOW()')->execute([$word, $category, $action]);
    audit($admin, 'moderation', 'add_sensitive_word', 'sensitive_word', $word);
    responseJson(['created' => true], 201, '敏感词已保存');
}

if ($method === 'DELETE' && preg_match('#^/v1/admin/sensitive-words/(\d+)$#', $path, $match)) {
    $admin = requireAdmin();
    db()->prepare('UPDATE sensitive_words SET enabled=0,updated_at=NOW() WHERE id=?')->execute([(int)$match[1]]);
    audit($admin, 'moderation', 'disable_sensitive_word', 'sensitive_word', $match[1]);
    responseJson(['disabled' => true], 200, '敏感词已停用');
}

if ($method === 'GET' && $path === '/v1/admin/audit-logs') {
    requireAdmin();
    $stmt = db()->query('SELECT l.id,l.module,l.action_name,l.target_type,l.target_id,l.details,l.ip_address,l.created_at,a.username,a.display_name FROM audit_logs l LEFT JOIN admin_users a ON a.id=l.admin_user_id ORDER BY l.id DESC LIMIT 500');
    responseJson(['items' => $stmt->fetchAll()]);
}

responseJson(null, 404, '接口不存在', 'not_found');
