AssetManager.API/AssetManager.Infrastructure/Services/TencentMarketService.cs
OpenClaw Agent 9014363d6d fix: 增强腾讯财经解析和日志
改进:
1. 修复引号解析逻辑(更健壮)
2. 减少字段验证从 36 到 5(只需价格字段)
3. 添加详细日志追踪解析过程
4. 记录请求 URL 和原始响应

日志关键词:
- [腾讯财经] 请求URL
- [腾讯财经] 原始响应
- [腾讯财经] 字段数量
- [腾讯财经] 成功
2026-03-24 09:52:25 +00:00

204 lines
7.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Text;
using System.Text.Json;
using AssetManager.Data;
using AssetManager.Models.DTOs;
using Microsoft.Extensions.Logging;
namespace AssetManager.Infrastructure.Services;
/// <summary>
/// 腾讯财经市场数据服务接口
/// </summary>
public interface ITencentMarketService
{
/// <summary>
/// 获取股票实时价格
/// </summary>
Task<MarketPriceResponse> GetStockPriceAsync(string symbol);
/// <summary>
/// 获取股票历史K线数据
/// <para>⚠️ 注意腾讯历史K线接口 (web.ifzq.gtimg.cn) 已废弃,此方法会抛出异常</para>
/// </summary>
[Obsolete("腾讯历史K线接口已废弃请使用 Yahoo 或 Tiingo")]
Task<List<MarketDataResponse>> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit);
}
/// <summary>
/// 腾讯财经市场数据服务实现
/// <para>实时价格接口 (qt.gtimg.cn) 正常可用</para>
/// <para>历史K线接口 (web.ifzq.gtimg.cn) 已废弃</para>
/// </summary>
public class TencentMarketService : ITencentMarketService
{
private readonly ILogger<TencentMarketService> _logger;
private readonly HttpClient _httpClient;
public TencentMarketService(ILogger<TencentMarketService> logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClient = httpClientFactory.CreateClient();
// 注册GBK编码支持
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
public async Task<MarketPriceResponse> GetStockPriceAsync(string symbol)
{
_logger.LogInformation("[腾讯财经] 获取股票价格: {Symbol}", symbol);
// 腾讯财经美股接口前缀us大写代码
var url = $"http://qt.gtimg.cn/q=us{symbol.ToUpper()}";
_logger.LogInformation("[腾讯财经] 请求URL: {Url}", url);
var responseBytes = await _httpClient.GetByteArrayAsync(url);
var response = Encoding.GetEncoding("GBK").GetString(responseBytes);
_logger.LogDebug("[腾讯财经] 原始响应: {Response}", response);
if (string.IsNullOrEmpty(response))
{
throw new Exception($"腾讯财经接口返回空数据,标的: {symbol}");
}
// 解析返回数据v_usUPRO="200~..." 格式
var quoteStart = response.IndexOf('"');
var quoteEnd = response.LastIndexOf('"');
if (quoteStart < 0 || quoteEnd <= quoteStart)
{
throw new Exception($"腾讯财经接口返回格式错误(无引号),标的: {symbol},响应: {response.Substring(0, Math.Min(200, response.Length))}");
}
var dataPart = response.Substring(quoteStart + 1, quoteEnd - quoteStart - 1);
var parts = dataPart.Split('~');
_logger.LogDebug("[腾讯财经] 字段数量: {Count}, parts[3]={Price}, parts[4]={PrevClose}", parts.Length, parts.Length > 3 ? parts[3] : "N/A", parts.Length > 4 ? parts[4] : "N/A");
if (parts.Length < 5)
{
throw new Exception($"腾讯财经返回字段不足,标的: {symbol},字段数: {parts.Length}");
}
// 提取字段:[3]=最新价 [4]=昨收价
if (!decimal.TryParse(parts[3], out var currentPrice) || currentPrice <= 0)
{
throw new Exception($"解析最新价失败,标的: {symbol},返回值: {parts[3]}");
}
if (!decimal.TryParse(parts[4], out var prevClose) || prevClose <= 0)
{
prevClose = currentPrice; // 解析失败用当前价当昨收
}
_logger.LogInformation("[腾讯财经] 成功: {Symbol} 最新价={CurrentPrice},昨收价={PrevClose}",
symbol, currentPrice, prevClose);
return new MarketPriceResponse
{
Symbol = symbol,
Price = currentPrice,
PreviousClose = prevClose,
Timestamp = DateTime.UtcNow,
AssetType = "Stock"
};
}
public async Task<List<MarketDataResponse>> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit)
{
_logger.LogInformation("获取腾讯财经历史数据: {Symbol}", symbol);
// 腾讯财经K线接口
var klineType = timeframe switch
{
"1d" or "daily" => "day",
"1w" or "weekly" => "week",
"1M" or "monthly" => "month",
_ => "day"
};
var url = $"http://web.ifzq.gtimg.cn/appstock/app/fqkline/get?_var=kline_{klineType}qfq&param=us{symbol.ToUpper()},{klineType},,{Math.Min(limit, 320)}&qrcode=1&asrd=1";
var responseBytes = await _httpClient.GetByteArrayAsync(url);
var response = Encoding.GetEncoding("GBK").GetString(responseBytes);
if (string.IsNullOrEmpty(response))
{
throw new Exception($"腾讯财经历史数据接口返回空数据,标的: {symbol}");
}
// 解析JSON
var jsonStart = response.IndexOf('{');
if (jsonStart < 0)
{
throw new Exception($"腾讯财经历史数据接口返回非JSON格式标的: {symbol}");
}
var jsonStr = response.Substring(jsonStart);
using var jsonDoc = JsonDocument.Parse(jsonStr);
var root = jsonDoc.RootElement;
// 检查API错误响应
if (root.TryGetProperty("code", out var codeEl) && codeEl.GetInt32() != 0)
{
var errMsg = root.TryGetProperty("msg", out var msgEl) ? msgEl.GetString() : "未知错误";
throw new Exception($"腾讯财经历史数据接口错误: {errMsg},标的: {symbol}");
}
// 检查 data 字段类型(错误响应时 data 是空数组 []
if (!root.TryGetProperty("data", out var data) || data.ValueKind != JsonValueKind.Object)
{
throw new Exception($"腾讯财经历史数据接口返回无效数据结构,标的: {symbol}");
}
// 数据路径: data -> us{symbol} -> qfq -> {klineType}
var dataPath = $"us{symbol.ToUpper()}";
if (!data.TryGetProperty(dataPath, out var stockData) ||
!stockData.TryGetProperty("qfq", out var qfq))
{
throw new Exception($"腾讯财经历史数据接口无该标的K线数据标的: {symbol}");
}
if (!qfq.TryGetProperty(klineType, out var klineData) || klineData.ValueKind != JsonValueKind.Array)
{
throw new Exception($"腾讯财经历史数据接口K线数据格式异常标的: {symbol}");
}
var result = new List<MarketDataResponse>();
foreach (var item in klineData.EnumerateArray())
{
// 数据格式: [日期, 开盘, 收盘, 最高, 最低, 成交量]
var arr = item.EnumerateArray().ToList();
if (arr.Count < 6) continue;
var dateStr = arr[0].GetString();
if (!DateTime.TryParse(dateStr, out var date)) continue;
var open = arr[1].GetDecimal();
var close = arr[2].GetDecimal();
var high = arr[3].GetDecimal();
var low = arr[4].GetDecimal();
var volume = arr[5].GetDecimal();
if (close <= 0) continue;
result.Add(new MarketDataResponse
{
Symbol = symbol,
Open = open,
High = high,
Low = low,
Close = close,
Volume = volume,
Timestamp = date,
AssetType = "Stock"
});
}
// 按日期排序
result = result.OrderBy(x => x.Timestamp).TakeLast(limit).ToList();
_logger.LogInformation("腾讯财经获取 {Symbol} 历史数据 {Count} 条", symbol, result.Count);
return result;
}
}