Skip to content
Commits on Source (2)
...@@ -24,7 +24,6 @@ dist-ssr ...@@ -24,7 +24,6 @@ dist-ssr
*.sw? *.sw?
# Environment variables # Environment variables
.env
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local
......
// 环境配置
export const environment = {
// API基础地址
API_BASE_URL: 'http://114.132.244.63/token-tushare',
// Tushare服务地址
TUSHARE_API_URL: 'http://114.132.244.63/api-tushare'
}
export default environment;
\ No newline at end of file
...@@ -69,6 +69,7 @@ ...@@ -69,6 +69,7 @@
</span> </span>
<div class="table-cell cell-actions"> <div class="table-cell cell-actions">
<button v-if="token.status === 'locked'" class="unlock-btn" @click="unlockToken(token)">解锁</button> <button v-if="token.status === 'locked'" class="unlock-btn" @click="unlockToken(token)">解锁</button>
<button class="issue-btn" @click="issueToken(token)">下发</button>
</div> </div>
</div> </div>
</div> </div>
...@@ -157,6 +158,8 @@ import { useRouter } from 'vue-router' ...@@ -157,6 +158,8 @@ import { useRouter } from 'vue-router'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import TopNavbar from '../components/TopNavbar.vue' import TopNavbar from '../components/TopNavbar.vue'
import SidebarMenu from '../components/SidebarMenu.vue' import SidebarMenu from '../components/SidebarMenu.vue'
import { environment } from '../config/environment'
const route = useRoute() const route = useRoute()
const isTokenMenuActive = computed(() => route.path === '/') const isTokenMenuActive = computed(() => route.path === '/')
const isUserMenuActive = computed(() => route.path === '/user-admin') const isUserMenuActive = computed(() => route.path === '/user-admin')
...@@ -219,7 +222,7 @@ async function addToken() { ...@@ -219,7 +222,7 @@ async function addToken() {
else if (newToken.period === '6m') validity_period = 6 else if (newToken.period === '6m') validity_period = 6
else if (newToken.period === '1y') validity_period = 12 else if (newToken.period === '1y') validity_period = 12
try { try {
const response = await fetch(`${API_BASE_URL}/api/token/create`, { const response = await fetch(`${environment.API_BASE_URL}/api/token/create`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
credentials: 'include', credentials: 'include',
...@@ -247,7 +250,20 @@ async function addToken() { ...@@ -247,7 +250,20 @@ async function addToken() {
} }
async function unlockToken(token) { async function unlockToken(token) {
try { try {
const response = await fetch(`${API_BASE_URL}/api/token/unlock`, { // 1. 先调用tushare服务的解锁接口
const tushareResponse = await fetch(`${environment.TUSHARE_API_URL}/tushare/unlock_token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: token.value })
})
const tushareRes = await tushareResponse.json()
if (!tushareRes.success) {
alert('tushare服务解锁失败: ' + (tushareRes.msg || '未知错误'))
return
}
// 2. 再调用本地后端解锁接口
const response = await fetch(`${environment.API_BASE_URL}/api/token/unlock`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
credentials: 'include', credentials: 'include',
...@@ -257,7 +273,24 @@ async function unlockToken(token) { ...@@ -257,7 +273,24 @@ async function unlockToken(token) {
if (res.success) { if (res.success) {
fetchTokens() fetchTokens()
} else { } else {
alert(res.message || '解锁失败') alert(res.message || '本地服务解锁失败')
}
} catch (e) {
alert('网络错误或服务器无响应')
}
}
async function issueToken(token) {
try {
const response = await fetch(`${environment.TUSHARE_API_URL}/tushare/add_token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: token.value })
})
const res = await response.json()
if (res.success) {
alert('下发成功')
} else {
alert(res.msg || '下发失败')
} }
} catch (e) { } catch (e) {
alert('网络错误或服务器无响应') alert('网络错误或服务器无响应')
...@@ -370,7 +403,7 @@ async function fetchTokens() { ...@@ -370,7 +403,7 @@ async function fetchTokens() {
if (statusFilter.value[0] === 'locked') params.append('is_locked', 'true') if (statusFilter.value[0] === 'locked') params.append('is_locked', 'true')
if (statusFilter.value[0] === 'normal') params.append('is_locked', 'false') if (statusFilter.value[0] === 'normal') params.append('is_locked', 'false')
} }
const response = await fetch(`${API_BASE_URL}/api/token/list?${params.toString()}`, { const response = await fetch(`${environment.API_BASE_URL}/api/token/list?${params.toString()}`, {
credentials: 'include' credentials: 'include'
}) })
const res = await response.json() const res = await response.json()
...@@ -391,7 +424,6 @@ async function fetchTokens() { ...@@ -391,7 +424,6 @@ async function fetchTokens() {
} }
} }
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL
const loading = ref(false) const loading = ref(false)
onMounted(() => { onMounted(() => {
...@@ -614,6 +646,20 @@ onBeforeUnmount(() => { ...@@ -614,6 +646,20 @@ onBeforeUnmount(() => {
background: #16a34a; background: #16a34a;
} }
.delete-btn { color: #ef4444; } .delete-btn { color: #ef4444; }
.issue-btn {
background: #3b82f6;
color: #fff;
border: none;
border-radius: 4px;
padding: 4px 16px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
margin-left: 8px;
}
.issue-btn:hover {
background: #2563eb;
}
.table-footer { .table-footer {
display: flex; display: flex;
......
...@@ -4,6 +4,7 @@ import vue from '@vitejs/plugin-vue' ...@@ -4,6 +4,7 @@ import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
base: '/tushare-web/', // 部署到子路径时必须加上
plugins: [vue()], plugins: [vue()],
resolve: { resolve: {
alias: { alias: {
......
...@@ -3,9 +3,12 @@ from app.service import tushare_funet ...@@ -3,9 +3,12 @@ from app.service import tushare_funet
from app.services import TokenService from app.services import TokenService
import inspect import inspect
from datetime import datetime, timedelta from datetime import datetime, timedelta
from app.utils.logger import get_logger
router = APIRouter(prefix="/tushare", tags=["tushare"]) router = APIRouter(prefix="/tushare", tags=["tushare"])
logger = get_logger("tushare_entry")
ALL_TOKENS = {} # key: token_value, value: token_info(完整字典) ALL_TOKENS = {} # key: token_value, value: token_info(完整字典)
TOKEN_IP_MAP = {} # token_value -> {'ip': ip, 'locked_at': datetime or None} TOKEN_IP_MAP = {} # token_value -> {'ip': ip, 'locked_at': datetime or None}
LOCKED_TOKENS = set() # 被锁定的token LOCKED_TOKENS = set() # 被锁定的token
...@@ -113,25 +116,39 @@ router.add_api_route("/pro_bar", pro_bar_view, methods=["POST"]) ...@@ -113,25 +116,39 @@ router.add_api_route("/pro_bar", pro_bar_view, methods=["POST"])
@router.post("") @router.post("")
async def tushare_entry(request: Request): async def tushare_entry(request: Request):
import time
start_time = time.time()
client_ip = request.client.host client_ip = request.client.host
t1 = time.time()
body = await request.json() body = await request.json()
t2 = time.time()
token = body.get("token") token = body.get("token")
ok, msg = check_token(token, client_ip) ok, msg = check_token(token, client_ip)
t3 = time.time()
if not ok: if not ok:
logger.info(f"[tushare_entry] token check failed, total: {time.time() - start_time:.4f}s, body: {t2-t1:.4f}s, check_token: {t3-t2:.4f}s")
return Response(content=msg, status_code=401) return Response(content=msg, status_code=401)
api_name = body.get("api_name") api_name = body.get("api_name")
t4 = time.time()
if not api_name: if not api_name:
logger.info(f"[tushare_entry] api_name missing, total: {time.time() - start_time:.4f}s, body: {t2-t1:.4f}s, check_token: {t3-t2:.4f}s, api_name: {t4-t3:.4f}s")
return {"success": False, "msg": "api_name 不能为空"} return {"success": False, "msg": "api_name 不能为空"}
# 动态分发 # 动态分发
if not hasattr(pro, api_name): if not hasattr(pro, api_name):
logger.info(f"[tushare_entry] api_name not supported, total: {time.time() - start_time:.4f}s, body: {t2-t1:.4f}s, check_token: {t3-t2:.4f}s, api_name: {t4-t3:.4f}s")
return {"success": False, "msg": f"不支持的api_name: {api_name}"} return {"success": False, "msg": f"不支持的api_name: {api_name}"}
method = getattr(pro, api_name) method = getattr(pro, api_name)
t5 = time.time()
try: try:
resp = method(**body) resp = method(**body)
t6 = time.time()
if hasattr(resp, "status_code") and hasattr(resp, "content"): if hasattr(resp, "status_code") and hasattr(resp, "content"):
logger.info(f"[tushare_entry] finished, total: {time.time() - start_time:.4f}s, body: {t2-t1:.4f}s, check_token: {t3-t2:.4f}s, api_name: {t4-t3:.4f}s, get_method: {t5-t4:.4f}s, method_call: {t6-t5:.4f}s, response: {time.time()-t6:.4f}s")
return Response(content=resp.content, status_code=resp.status_code, headers=dict(resp.headers), media_type=resp.headers.get("content-type", None)) return Response(content=resp.content, status_code=resp.status_code, headers=dict(resp.headers), media_type=resp.headers.get("content-type", None))
logger.info(f"[tushare_entry] finished, total: {time.time() - start_time:.4f}s, body: {t2-t1:.4f}s, check_token: {t3-t2:.4f}s, api_name: {t4-t3:.4f}s, get_method: {t5-t4:.4f}s, method_call: {t6-t5:.4f}s, response: {time.time()-t6:.4f}s")
return resp return resp
except Exception as e: except Exception as e:
logger.error(f"[tushare_entry] exception, total: {time.time() - start_time:.4f}s, body: {t2-t1:.4f}s, check_token: {t3-t2:.4f}s, api_name: {t4-t3:.4f}s, get_method: {t5-t4:.4f}s, exception: {str(e)}")
return Response(content=str(e), status_code=500) return Response(content=str(e), status_code=500)
@router.post("/unlock_token") @router.post("/unlock_token")
......
fastapi fastapi
uvicorn uvicorn[standard]
pydantic pydantic
sqlalchemy
pymysql
requests
pandas
python-dateutil
python-dotenv
\ No newline at end of file
import os
import requests
import pytest
import tushare as ts
import tushare_data as ts1
import pandas as pd
TUSHARE_TOKEN = os.getenv('TS_TOKEN', 'le2937d38d26f5322ae6096286072faf933')
MY_TOKEN = os.getenv('MY_TOKEN', '1ab08efbf57546eab5a62499848c542a')
# tushare官方接口列表(可根据需要补充)
TUSHARE_APIS = [
('stock_basic', {'exchange': '', 'list_status': 'L', 'fields': 'ts_code,symbol,name,area,industry,list_date'}),
# ('daily', {...}),
# ... 其他接口及参数
]
pro = ts.pro_api(TUSHARE_TOKEN)
pro1 = ts1.pro_api(MY_TOKEN)
def compare_result(my_result, tushare_result):
"""
严格对比两个DataFrame的字段、行数、所有单元格的值。
"""
# 字段名对比
assert list(my_result.columns) == list(tushare_result.columns), f"字段不一致: {list(my_result.columns)} vs {list(tushare_result.columns)}"
# 行数对比
assert len(my_result) == len(tushare_result), f"行数不一致: {len(my_result)} vs {len(tushare_result)}"
# 所有值对比(忽略索引和数据类型差异)
pd.testing.assert_frame_equal(
my_result.reset_index(drop=True),
tushare_result.reset_index(drop=True),
check_dtype=False,
check_like=True,
check_exact=False # 允许浮点误差
)
def test_stock_basic():
tushare_result = ts.stock_basic(exchange='', list_status='L', fields='ts_code,symbol,name,area,industry,list_date')
my_result = ts1.stock_basic(exchange='', list_status='L', fields='ts_code,symbol,name,area,industry,list_date')
compare_result(my_result, tushare_result)
\ No newline at end of file
This diff is collapsed.