Skip to content
GitLab
Explore
Sign in
Register
Commits on Source (2)
增加解锁下发
· 3a7fb951
leewcc
authored
Jul 22, 2025
3a7fb951
Merge branch 'dev' of www.fuconx.com:fuconx/tushareweb into dev
· fbe10f1a
leewcc
authored
Jul 22, 2025
fbe10f1a
Expand all
Show whitespace changes
Inline
Side-by-side
tushare-token-front/.gitignore
View file @
fbe10f1a
...
@@ -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
...
...
tushare-token-front/src/config/environment.js
0 → 100644
View file @
fbe10f1a
// 环境配置
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
tushare-token-front/src/views/AdminView.vue
View file @
fbe10f1a
...
@@ -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
;
...
...
tushare-token-front/vite.config.js
View file @
fbe10f1a
...
@@ -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
:
{
...
...
tushare-web-api/app/api/tushare.py
View file @
fbe10f1a
...
@@ -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
:
.
4
f
}
s, body:
{
t2
-
t1
:
.
4
f
}
s, check_token:
{
t3
-
t2
:
.
4
f
}
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
:
.
4
f
}
s, body:
{
t2
-
t1
:
.
4
f
}
s, check_token:
{
t3
-
t2
:
.
4
f
}
s, api_name:
{
t4
-
t3
:
.
4
f
}
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
:
.
4
f
}
s, body:
{
t2
-
t1
:
.
4
f
}
s, check_token:
{
t3
-
t2
:
.
4
f
}
s, api_name:
{
t4
-
t3
:
.
4
f
}
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
:
.
4
f
}
s, body:
{
t2
-
t1
:
.
4
f
}
s, check_token:
{
t3
-
t2
:
.
4
f
}
s, api_name:
{
t4
-
t3
:
.
4
f
}
s, get_method:
{
t5
-
t4
:
.
4
f
}
s, method_call:
{
t6
-
t5
:
.
4
f
}
s, response:
{
time
.
time
()
-
t6
:
.
4
f
}
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
:
.
4
f
}
s, body:
{
t2
-
t1
:
.
4
f
}
s, check_token:
{
t3
-
t2
:
.
4
f
}
s, api_name:
{
t4
-
t3
:
.
4
f
}
s, get_method:
{
t5
-
t4
:
.
4
f
}
s, method_call:
{
t6
-
t5
:
.
4
f
}
s, response:
{
time
.
time
()
-
t6
:
.
4
f
}
s
"
)
return
resp
return
resp
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"
[tushare_entry] exception, total:
{
time
.
time
()
-
start_time
:
.
4
f
}
s, body:
{
t2
-
t1
:
.
4
f
}
s, check_token:
{
t3
-
t2
:
.
4
f
}
s, api_name:
{
t4
-
t3
:
.
4
f
}
s, get_method:
{
t5
-
t4
:
.
4
f
}
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
"
)
...
...
tushare-web-api/requirements.txt
View file @
fbe10f1a
fastapi
fastapi
uvicorn
uvicorn
[standard]
pydantic
pydantic
sqlalchemy
pymysql
requests
pandas
python-dateutil
python-dotenv
\ No newline at end of file
tushare-web-api/test/test_tushare_api.py
0 → 100644
View file @
fbe10f1a
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
tushare-web-api/test/tushare_data.py
0 → 100644
View file @
fbe10f1a
This diff is collapsed.
Click to expand it.