feat: 初始化智能选股分析系统微服务架构

添加核心微服务组件包括数据服务、量化服务、情绪分析服务等
实现基于Consul的服务发现和Traefik网关路由
包含Docker化部署方案和CI/CD Webhook支持
This commit is contained in:
fanfpy 2025-06-21 15:40:19 +08:00
commit aa09c92ad5
28 changed files with 384 additions and 0 deletions

29
.gitignore vendored Normal file
View 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
View 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
View File

78
docker-compose.yml Normal file
View 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
View 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
View 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)

View 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"]

View 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)

View 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}")

View File

@ -0,0 +1 @@
flask

View 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"]

View 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)

View 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}")

View File

@ -0,0 +1 @@
flask

View 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;"]

View 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"
}
}

View 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>

View 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"]

View 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)

View 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}")

View File

@ -0,0 +1 @@
flask

View 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>

View 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"]

View 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");

View 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>

View 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"]

View 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
View File

@ -0,0 +1,6 @@
api:
dashboard: true
entryPoints:
web:
address: ":80"