feat: 初始化智能选股分析系统微服务架构
添加核心微服务组件包括数据服务、量化服务、情绪分析服务等 实现基于Consul的服务发现和Traefik网关路由 包含Docker化部署方案和CI/CD Webhook支持
This commit is contained in:
commit
aa09c92ad5
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.egg
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# C#
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
*.tar
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# VSCode
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
20
README.md
Normal file
20
README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# AriStockAI
|
||||||
|
|
||||||
|
面向个人投资者的智能选股分析系统,采用微服务架构构建。
|
||||||
|
|
||||||
|
## 服务一览
|
||||||
|
- data-service:提供 A 股 / 美股行情数据(Python)
|
||||||
|
- quant-service:量化因子计算服务(Python)
|
||||||
|
- emotion-service:情绪分析服务(Python)
|
||||||
|
- recommend-service:融合推荐服务(C#)
|
||||||
|
- user-service:用户管理服务(C#)
|
||||||
|
- frontend:前端交互界面(Vue.js)
|
||||||
|
- gitea-webhook:监听 Gitea Webhook 进行持续部署
|
||||||
|
- traefik:作为统一网关实现服务发现和路由
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
```bash
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
访问入口:`http://localhost` 或你的服务器域名(如 AriStockAI.com)
|
||||||
0
consul/config/consul.hcl
Normal file
0
consul/config/consul.hcl
Normal file
78
docker-compose.yml
Normal file
78
docker-compose.yml
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v2.9
|
||||||
|
command:
|
||||||
|
- --entrypoints.web.address=:80
|
||||||
|
- --providers.consulcatalog=true
|
||||||
|
- --providers.consulcatalog.endpoint.address=consul:8500
|
||||||
|
- --api.dashboard=true
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- ./traefik/traefik.yml:/etc/traefik/traefik.yml
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
|
||||||
|
data-service:
|
||||||
|
build: ./services/data-service
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.data.rule=PathPrefix(`/api/data`)"
|
||||||
|
expose:
|
||||||
|
- "8000"
|
||||||
|
|
||||||
|
quant-service:
|
||||||
|
build: ./services/quant-service
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.quant.rule=PathPrefix(`/api/quant`)"
|
||||||
|
expose:
|
||||||
|
- "8001"
|
||||||
|
|
||||||
|
emotion-service:
|
||||||
|
build: ./services/emotion-service
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.emotion.rule=PathPrefix(`/api/emotion`)"
|
||||||
|
expose:
|
||||||
|
- "8002"
|
||||||
|
|
||||||
|
recommend-service:
|
||||||
|
build: ./services/recommend-service
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.recommend.rule=PathPrefix(`/api/recommend`)"
|
||||||
|
expose:
|
||||||
|
- "8003"
|
||||||
|
|
||||||
|
user-service:
|
||||||
|
build: ./services/user-service
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.user.rule=PathPrefix(`/api/user`)"
|
||||||
|
expose:
|
||||||
|
- "8004"
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build: ./services/frontend
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.frontend.rule=PathPrefix(`/`)"
|
||||||
|
expose:
|
||||||
|
- "80"
|
||||||
|
|
||||||
|
gitea-webhook:
|
||||||
|
build: ./gitea-webhook
|
||||||
|
ports:
|
||||||
|
- "5005:5005"
|
||||||
|
|
||||||
|
consul:
|
||||||
|
image: consul:1.15
|
||||||
|
ports:
|
||||||
|
- "8500:8500" # Consul Web UI
|
||||||
|
- "8600:8600/udp"
|
||||||
|
volumes:
|
||||||
|
- ./consul/config:/consul/config
|
||||||
|
command: "consul agent -dev -config-dir=/consul/config"
|
||||||
5
gitea-webhook/Dockerfile
Normal file
5
gitea-webhook/Dockerfile
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY webhook.py .
|
||||||
|
RUN pip install flask
|
||||||
|
CMD ["python", "webhook.py"]
|
||||||
9
gitea-webhook/webhook.py
Normal file
9
gitea-webhook/webhook.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from flask import Flask, request
|
||||||
|
import os, subprocess
|
||||||
|
app = Flask(__name__)
|
||||||
|
@app.route('/webhook', methods=['POST'])
|
||||||
|
def webhook():
|
||||||
|
subprocess.Popen(["/bin/sh", "./deploy.sh"])
|
||||||
|
return 'Triggered', 200
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=5005)
|
||||||
7
services/data-service/Dockerfile
Normal file
7
services/data-service/Dockerfile
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
## services/data-service/Dockerfile
|
||||||
|
FROM python:3.10-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
COPY . .
|
||||||
|
CMD ["python", "app.py"]
|
||||||
17
services/data-service/app.py
Normal file
17
services/data-service/app.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from flask import Flask, jsonify
|
||||||
|
import threading
|
||||||
|
from register import register_to_consul
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
@app.route("/health")
|
||||||
|
def health():
|
||||||
|
return jsonify({"status": "ok"}), 200
|
||||||
|
|
||||||
|
@app.route("/hello")
|
||||||
|
def hello():
|
||||||
|
return jsonify({"message": "Hello from service!"})
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
threading.Thread(target=register_to_consul).start()
|
||||||
|
app.run(host="0.0.0.0", port=8000)
|
||||||
24
services/data-service/register.py
Normal file
24
services/data-service/register.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import time
|
||||||
|
import requests
|
||||||
|
|
||||||
|
def register_to_consul():
|
||||||
|
time.sleep(3) # 等待服务稳定再注册
|
||||||
|
service_name = "data-service" # 替换为 quant-service/emotion-service 等
|
||||||
|
port = 8000 # 替换为 8001, 8002 等
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"Name": service_name,
|
||||||
|
"ID": f"{service_name}-1",
|
||||||
|
"Address": service_name, # 容器名作为地址
|
||||||
|
"Port": port,
|
||||||
|
"Check": {
|
||||||
|
"HTTP": f"http://{service_name}:{port}/health",
|
||||||
|
"Interval": "10s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
requests.put("http://consul:8500/v1/agent/service/register", json=payload)
|
||||||
|
print(f"[{service_name}] Registered to Consul")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Consul register failed: {e}")
|
||||||
1
services/data-service/requirements.txt
Normal file
1
services/data-service/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
flask
|
||||||
6
services/emotion-service/Dockerfile
Normal file
6
services/emotion-service/Dockerfile
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
COPY . .
|
||||||
|
CMD ["python", "app.py"]
|
||||||
17
services/emotion-service/app.py
Normal file
17
services/emotion-service/app.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from flask import Flask, jsonify
|
||||||
|
import threading
|
||||||
|
from register import register_to_consul
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
@app.route("/health")
|
||||||
|
def health():
|
||||||
|
return jsonify({"status": "ok"}), 200
|
||||||
|
|
||||||
|
@app.route("/hello")
|
||||||
|
def hello():
|
||||||
|
return jsonify({"message": "Hello from service!"})
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
threading.Thread(target=register_to_consul).start()
|
||||||
|
app.run(host="0.0.0.0", port=8002)
|
||||||
24
services/emotion-service/register.py
Normal file
24
services/emotion-service/register.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import time
|
||||||
|
import requests
|
||||||
|
|
||||||
|
def register_to_consul():
|
||||||
|
time.sleep(3) # 等待服务稳定再注册
|
||||||
|
service_name = "emotion-service" # 替换为 quant-service/emotion-service 等
|
||||||
|
port = 8002 # 替换为 8001, 8002 等
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"Name": service_name,
|
||||||
|
"ID": f"{service_name}-1",
|
||||||
|
"Address": service_name, # 容器名作为地址
|
||||||
|
"Port": port,
|
||||||
|
"Check": {
|
||||||
|
"HTTP": f"http://{service_name}:{port}/health",
|
||||||
|
"Interval": "10s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
requests.put("http://consul:8500/v1/agent/service/register", json=payload)
|
||||||
|
print(f"[{service_name}] Registered to Consul")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Consul register failed: {e}")
|
||||||
1
services/emotion-service/requirements.txt
Normal file
1
services/emotion-service/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
flask
|
||||||
11
services/frontend/Dockerfile
Normal file
11
services/frontend/Dockerfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
FROM node:16-alpine as build-stage
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:stable-alpine as production-stage
|
||||||
|
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
21
services/frontend/package.json
Normal file
21
services/frontend/package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "aristockai-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.2.45",
|
||||||
|
"vue-router": "^4.1.6",
|
||||||
|
"axios": "^1.3.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-service": "~5.0.8",
|
||||||
|
"@vue/compiler-sfc": "^3.2.45",
|
||||||
|
"eslint": "^8.34.0",
|
||||||
|
"eslint-plugin-vue": "^9.9.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
services/frontend/public/index.html
Normal file
16
services/frontend/public/index.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>AriStockAI</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but AriStockAI doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6
services/quant-service/Dockerfile
Normal file
6
services/quant-service/Dockerfile
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
COPY . .
|
||||||
|
CMD ["python", "app.py"]
|
||||||
17
services/quant-service/app.py
Normal file
17
services/quant-service/app.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from flask import Flask, jsonify
|
||||||
|
import threading
|
||||||
|
from register import register_to_consul
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
@app.route("/health")
|
||||||
|
def health():
|
||||||
|
return jsonify({"status": "ok"}), 200
|
||||||
|
|
||||||
|
@app.route("/hello")
|
||||||
|
def hello():
|
||||||
|
return jsonify({"message": "Hello from service!"})
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
threading.Thread(target=register_to_consul).start()
|
||||||
|
app.run(host="0.0.0.0", port=8001)
|
||||||
24
services/quant-service/register.py
Normal file
24
services/quant-service/register.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import time
|
||||||
|
import requests
|
||||||
|
|
||||||
|
def register_to_consul():
|
||||||
|
time.sleep(3) # 等待服务稳定再注册
|
||||||
|
service_name = "quant-service" # 替换为 quant-service/emotion-service 等
|
||||||
|
port = 8001 # 替换为 8001, 8002 等
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"Name": service_name,
|
||||||
|
"ID": f"{service_name}-1",
|
||||||
|
"Address": service_name, # 容器名作为地址
|
||||||
|
"Port": port,
|
||||||
|
"Check": {
|
||||||
|
"HTTP": f"http://{service_name}:{port}/health",
|
||||||
|
"Interval": "10s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
requests.put("http://consul:8500/v1/agent/service/register", json=payload)
|
||||||
|
print(f"[{service_name}] Registered to Consul")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Consul register failed: {e}")
|
||||||
1
services/quant-service/requirements.txt
Normal file
1
services/quant-service/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
flask
|
||||||
10
services/recommend-service/AriStockAI.Recommend.csproj
Normal file
10
services/recommend-service/AriStockAI.Recommend.csproj
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
5
services/recommend-service/Dockerfile
Normal file
5
services/recommend-service/Dockerfile
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
EXPOSE 8003
|
||||||
|
ENTRYPOINT ["dotnet", "AriStockAI.Recommend.dll"]
|
||||||
7
services/recommend-service/Program.cs
Normal file
7
services/recommend-service/Program.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
var app = builder.Build();
|
||||||
|
app.MapGet("/", () => new { service = "recommend-service", status = "ok" });
|
||||||
|
app.Run("http://0.0.0.0:8003");
|
||||||
10
services/user-service/AriStockAI.User.csproj
Normal file
10
services/user-service/AriStockAI.User.csproj
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
5
services/user-service/Dockerfile
Normal file
5
services/user-service/Dockerfile
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
EXPOSE 8004
|
||||||
|
ENTRYPOINT ["dotnet", "AriStockAI.User.dll"]
|
||||||
7
services/user-service/Program.cs
Normal file
7
services/user-service/Program.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
var app = builder.Build();
|
||||||
|
app.MapGet("/", () => new { service = "user-service", status = "ok" });
|
||||||
|
app.Run("http://0.0.0.0:8004");
|
||||||
6
traefik/traefik.yml
Normal file
6
traefik/traefik.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
api:
|
||||||
|
dashboard: true
|
||||||
|
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80"
|
||||||
Loading…
Reference in New Issue
Block a user