From aa09c92ad5c13ced2ac2409b00d1ea5117ff6012 Mon Sep 17 00:00:00 2001 From: fanfpy Date: Sat, 21 Jun 2025 15:40:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E9=80=89=E8=82=A1=E5=88=86=E6=9E=90=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E5=BE=AE=E6=9C=8D=E5=8A=A1=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加核心微服务组件包括数据服务、量化服务、情绪分析服务等 实现基于Consul的服务发现和Traefik网关路由 包含Docker化部署方案和CI/CD Webhook支持 --- .gitignore | 29 +++++++ README.md | 20 +++++ consul/config/consul.hcl | 0 docker-compose.yml | 78 +++++++++++++++++++ gitea-webhook/Dockerfile | 5 ++ gitea-webhook/webhook.py | 9 +++ services/data-service/Dockerfile | 7 ++ services/data-service/app.py | 17 ++++ services/data-service/register.py | 24 ++++++ services/data-service/requirements.txt | 1 + services/emotion-service/Dockerfile | 6 ++ services/emotion-service/app.py | 17 ++++ services/emotion-service/register.py | 24 ++++++ services/emotion-service/requirements.txt | 1 + services/frontend/Dockerfile | 11 +++ services/frontend/package.json | 21 +++++ services/frontend/public/index.html | 16 ++++ services/quant-service/Dockerfile | 6 ++ services/quant-service/app.py | 17 ++++ services/quant-service/register.py | 24 ++++++ services/quant-service/requirements.txt | 1 + .../AriStockAI.Recommend.csproj | 10 +++ services/recommend-service/Dockerfile | 5 ++ services/recommend-service/Program.cs | 7 ++ services/user-service/AriStockAI.User.csproj | 10 +++ services/user-service/Dockerfile | 5 ++ services/user-service/Program.cs | 7 ++ traefik/traefik.yml | 6 ++ 28 files changed, 384 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 consul/config/consul.hcl create mode 100644 docker-compose.yml create mode 100644 gitea-webhook/Dockerfile create mode 100644 gitea-webhook/webhook.py create mode 100644 services/data-service/Dockerfile create mode 100644 services/data-service/app.py create mode 100644 services/data-service/register.py create mode 100644 services/data-service/requirements.txt create mode 100644 services/emotion-service/Dockerfile create mode 100644 services/emotion-service/app.py create mode 100644 services/emotion-service/register.py create mode 100644 services/emotion-service/requirements.txt create mode 100644 services/frontend/Dockerfile create mode 100644 services/frontend/package.json create mode 100644 services/frontend/public/index.html create mode 100644 services/quant-service/Dockerfile create mode 100644 services/quant-service/app.py create mode 100644 services/quant-service/register.py create mode 100644 services/quant-service/requirements.txt create mode 100644 services/recommend-service/AriStockAI.Recommend.csproj create mode 100644 services/recommend-service/Dockerfile create mode 100644 services/recommend-service/Program.cs create mode 100644 services/user-service/AriStockAI.User.csproj create mode 100644 services/user-service/Dockerfile create mode 100644 services/user-service/Program.cs create mode 100644 traefik/traefik.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a7933e --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..2564b97 --- /dev/null +++ b/README.md @@ -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) diff --git a/consul/config/consul.hcl b/consul/config/consul.hcl new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a75397a --- /dev/null +++ b/docker-compose.yml @@ -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" diff --git a/gitea-webhook/Dockerfile b/gitea-webhook/Dockerfile new file mode 100644 index 0000000..36bcbe6 --- /dev/null +++ b/gitea-webhook/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.10-slim +WORKDIR /app +COPY webhook.py . +RUN pip install flask +CMD ["python", "webhook.py"] \ No newline at end of file diff --git a/gitea-webhook/webhook.py b/gitea-webhook/webhook.py new file mode 100644 index 0000000..a5b30db --- /dev/null +++ b/gitea-webhook/webhook.py @@ -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) \ No newline at end of file diff --git a/services/data-service/Dockerfile b/services/data-service/Dockerfile new file mode 100644 index 0000000..da16c83 --- /dev/null +++ b/services/data-service/Dockerfile @@ -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"] diff --git a/services/data-service/app.py b/services/data-service/app.py new file mode 100644 index 0000000..18deafa --- /dev/null +++ b/services/data-service/app.py @@ -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) diff --git a/services/data-service/register.py b/services/data-service/register.py new file mode 100644 index 0000000..c0083e3 --- /dev/null +++ b/services/data-service/register.py @@ -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}") diff --git a/services/data-service/requirements.txt b/services/data-service/requirements.txt new file mode 100644 index 0000000..8ab6294 --- /dev/null +++ b/services/data-service/requirements.txt @@ -0,0 +1 @@ +flask \ No newline at end of file diff --git a/services/emotion-service/Dockerfile b/services/emotion-service/Dockerfile new file mode 100644 index 0000000..f5f0e88 --- /dev/null +++ b/services/emotion-service/Dockerfile @@ -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"] diff --git a/services/emotion-service/app.py b/services/emotion-service/app.py new file mode 100644 index 0000000..29b1ae8 --- /dev/null +++ b/services/emotion-service/app.py @@ -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) diff --git a/services/emotion-service/register.py b/services/emotion-service/register.py new file mode 100644 index 0000000..310324c --- /dev/null +++ b/services/emotion-service/register.py @@ -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}") diff --git a/services/emotion-service/requirements.txt b/services/emotion-service/requirements.txt new file mode 100644 index 0000000..8ab6294 --- /dev/null +++ b/services/emotion-service/requirements.txt @@ -0,0 +1 @@ +flask \ No newline at end of file diff --git a/services/frontend/Dockerfile b/services/frontend/Dockerfile new file mode 100644 index 0000000..fc9f0ef --- /dev/null +++ b/services/frontend/Dockerfile @@ -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;"] \ No newline at end of file diff --git a/services/frontend/package.json b/services/frontend/package.json new file mode 100644 index 0000000..aa98eaf --- /dev/null +++ b/services/frontend/package.json @@ -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" + } +} \ No newline at end of file diff --git a/services/frontend/public/index.html b/services/frontend/public/index.html new file mode 100644 index 0000000..72328e1 --- /dev/null +++ b/services/frontend/public/index.html @@ -0,0 +1,16 @@ + + + + + + + AriStockAI + + + +
+ + + \ No newline at end of file diff --git a/services/quant-service/Dockerfile b/services/quant-service/Dockerfile new file mode 100644 index 0000000..a7914a6 --- /dev/null +++ b/services/quant-service/Dockerfile @@ -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"] \ No newline at end of file diff --git a/services/quant-service/app.py b/services/quant-service/app.py new file mode 100644 index 0000000..e672de8 --- /dev/null +++ b/services/quant-service/app.py @@ -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) diff --git a/services/quant-service/register.py b/services/quant-service/register.py new file mode 100644 index 0000000..dea2549 --- /dev/null +++ b/services/quant-service/register.py @@ -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}") diff --git a/services/quant-service/requirements.txt b/services/quant-service/requirements.txt new file mode 100644 index 0000000..8ab6294 --- /dev/null +++ b/services/quant-service/requirements.txt @@ -0,0 +1 @@ +flask \ No newline at end of file diff --git a/services/recommend-service/AriStockAI.Recommend.csproj b/services/recommend-service/AriStockAI.Recommend.csproj new file mode 100644 index 0000000..26ae0c0 --- /dev/null +++ b/services/recommend-service/AriStockAI.Recommend.csproj @@ -0,0 +1,10 @@ + + + net6.0 + enable + enable + + + + + \ No newline at end of file diff --git a/services/recommend-service/Dockerfile b/services/recommend-service/Dockerfile new file mode 100644 index 0000000..d7289c6 --- /dev/null +++ b/services/recommend-service/Dockerfile @@ -0,0 +1,5 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +COPY . . +EXPOSE 8003 +ENTRYPOINT ["dotnet", "AriStockAI.Recommend.dll"] \ No newline at end of file diff --git a/services/recommend-service/Program.cs b/services/recommend-service/Program.cs new file mode 100644 index 0000000..43b8a29 --- /dev/null +++ b/services/recommend-service/Program.cs @@ -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"); \ No newline at end of file diff --git a/services/user-service/AriStockAI.User.csproj b/services/user-service/AriStockAI.User.csproj new file mode 100644 index 0000000..26ae0c0 --- /dev/null +++ b/services/user-service/AriStockAI.User.csproj @@ -0,0 +1,10 @@ + + + net6.0 + enable + enable + + + + + \ No newline at end of file diff --git a/services/user-service/Dockerfile b/services/user-service/Dockerfile new file mode 100644 index 0000000..fe1b47c --- /dev/null +++ b/services/user-service/Dockerfile @@ -0,0 +1,5 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +COPY . . +EXPOSE 8004 +ENTRYPOINT ["dotnet", "AriStockAI.User.dll"] \ No newline at end of file diff --git a/services/user-service/Program.cs b/services/user-service/Program.cs new file mode 100644 index 0000000..7058efe --- /dev/null +++ b/services/user-service/Program.cs @@ -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"); \ No newline at end of file diff --git a/traefik/traefik.yml b/traefik/traefik.yml new file mode 100644 index 0000000..a6db39b --- /dev/null +++ b/traefik/traefik.yml @@ -0,0 +1,6 @@ +api: + dashboard: true + +entryPoints: + web: + address: ":80" \ No newline at end of file