first commit
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
# 版本控制
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# 编辑器
|
||||
.vscode
|
||||
|
||||
# 构建与配置文件(非运行时必需)
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
README.md
|
||||
*.md
|
||||
*.log
|
||||
*.log.*
|
||||
|
||||
# 开发依赖
|
||||
web/node_modules
|
||||
node_modules
|
||||
|
||||
# Python 缓存与临时文件
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# 测试
|
||||
tests/
|
||||
test_*.py
|
||||
|
||||
# 环境文件(通过 volume 挂载,不进镜像)
|
||||
.env
|
||||
.env.*
|
||||
env/
|
||||
|
||||
# 部署脚本(只拷贝需要的 entrypoint.sh,其他 deploy/ 内容不进)
|
||||
deploy/
|
||||
!deploy/entrypoint.sh
|
||||
!deploy/web.conf
|
||||
|
||||
# 其他
|
||||
*.swp
|
||||
*.tmp
|
||||
Thumbs.db
|
||||
@@ -0,0 +1 @@
|
||||
*.html linguist-language=python
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
__pycache__/
|
||||
.idea/
|
||||
venv/
|
||||
.mypy_cache/
|
||||
.vscode
|
||||
.ruff_cache/
|
||||
.pytest_cache/
|
||||
migrations/
|
||||
|
||||
db*.sqlite3
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
db.sqlite3-shm
|
||||
db.sqlite3-wal
|
||||
|
||||
.DS_Store
|
||||
._.DS_Store
|
||||
|
||||
|
||||
*.zip
|
||||
*.tar.gz
|
||||
tmp*
|
||||
tmp/*
|
||||
*.sqlite3-shm
|
||||
*.sqlite3-wal
|
||||
outputs/
|
||||
uploads/
|
||||
|
||||
|
||||
env/*
|
||||
!env/*example.py
|
||||
*.pkl
|
||||
scripts/setup.sh
|
||||
*.log
|
||||
*.log.*
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
FROM node:18.12.0-alpine3.16 AS web
|
||||
|
||||
WORKDIR /opt/build
|
||||
COPY /web ./web
|
||||
RUN npm install pnpm -g --registry=https://registry.npmmirror.com && cd /opt/build/web && pnpm i --registry=https://registry.npmmirror.com && pnpm run build
|
||||
|
||||
# === Python 依赖构建阶段(带编译工具)===
|
||||
FROM python:3.11-slim-bullseye AS builder
|
||||
|
||||
# 切换为中科大源
|
||||
RUN sed -i "s@http://.*.debian.org@http://mirrors.ustc.edu.cn@g" /etc/apt/sources.list \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends gcc python3-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 创建虚拟环境
|
||||
RUN python -m venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
# 安装 Python 依赖(使用阿里云源,避免清华限流)
|
||||
ADD app ./app
|
||||
COPY requirements.txt ./app
|
||||
RUN find app -name "requirement*.txt" | xargs -n 1 pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ -r
|
||||
|
||||
FROM python:3.11-slim-bullseye
|
||||
WORKDIR /opt/backend-server
|
||||
|
||||
# 设置时区 & 安装最小运行依赖
|
||||
RUN sed -i 's@http://.*.debian.org@http://mirrors.ustc.edu.cn@g' /etc/apt/sources.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nginx tzdata bash curl \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& echo "Asia/Shanghai" > /etc/timezone
|
||||
|
||||
# 从 builder 阶段拷贝已安装的 Python 依赖
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
|
||||
# 激活虚拟环境(通过 PATH)
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
ENV LANG=zh_CN.UTF-8
|
||||
|
||||
# 拷贝应用代码(只拷贝必需目录和文件)
|
||||
COPY app ./app
|
||||
COPY static ./static
|
||||
COPY templates ./templates
|
||||
COPY run.py .
|
||||
COPY deploy/entrypoint.sh .
|
||||
|
||||
# 拷贝前端产物
|
||||
COPY --from=web /opt/build/web/dist ./web/dist
|
||||
|
||||
# Nginx 配置
|
||||
COPY deploy/web.conf /etc/nginx/sites-available/web.conf
|
||||
RUN rm -f /etc/nginx/sites-enabled/default \
|
||||
&& ln -s /etc/nginx/sites-available/web.conf /etc/nginx/sites-enabled/web.conf; \
|
||||
python -c "import sys; print(sys.path)"
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
ENTRYPOINT ["sh", "entrypoint.sh"]
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 mizhexiaoxiao
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,87 @@
|
||||
# Build configuration
|
||||
# -------------------
|
||||
|
||||
APP_NAME := `sed -n 's/^ *name.*=.*"\([^"]*\)".*/\1/p' pyproject.toml`
|
||||
APP_VERSION := `sed -n 's/^ *version.*=.*"\([^"]*\)".*/\1/p' pyproject.toml`
|
||||
GIT_REVISION = `git rev-parse HEAD`
|
||||
|
||||
# Introspection targets
|
||||
# ---------------------
|
||||
|
||||
.PHONY: help
|
||||
help: header targets
|
||||
|
||||
.PHONY: header
|
||||
header:
|
||||
@echo "\033[34mEnvironment\033[0m"
|
||||
@echo "\033[34m---------------------------------------------------------------\033[0m"
|
||||
@printf "\033[33m%-23s\033[0m" "APP_NAME"
|
||||
@printf "\033[35m%s\033[0m" $(APP_NAME)
|
||||
@echo ""
|
||||
@printf "\033[33m%-23s\033[0m" "APP_VERSION"
|
||||
@printf "\033[35m%s\033[0m" $(APP_VERSION)
|
||||
@echo ""
|
||||
@printf "\033[33m%-23s\033[0m" "GIT_REVISION"
|
||||
@printf "\033[35m%s\033[0m" $(GIT_REVISION)
|
||||
@echo "\n"
|
||||
|
||||
.PHONY: targets
|
||||
targets:
|
||||
@echo "\033[34mDevelopment Targets\033[0m"
|
||||
@echo "\033[34m---------------------------------------------------------------\033[0m"
|
||||
@perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
# Development targets
|
||||
# -------------
|
||||
|
||||
.PHONY: install
|
||||
install: ## Install dependencies
|
||||
uv add pyproject.toml
|
||||
|
||||
|
||||
.PHONY: run
|
||||
run: start
|
||||
|
||||
.PHONY: start
|
||||
start: ## Starts the server
|
||||
python run.py
|
||||
|
||||
# Check, lint and format targets
|
||||
# ------------------------------
|
||||
|
||||
.PHONY: check
|
||||
check: check-format lint
|
||||
|
||||
.PHONY: check-format
|
||||
check-format: ## Dry-run code formatter
|
||||
black ./ --check
|
||||
isort ./ --profile black --check
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Run ruff
|
||||
ruff check ./app
|
||||
|
||||
.PHONY: format
|
||||
format: ## Run code formatter
|
||||
black ./
|
||||
isort ./ --profile black
|
||||
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run the test suite
|
||||
$(eval include .env)
|
||||
$(eval export $(sh sed 's/=.*//' .env))
|
||||
pytest -vv -s --cache-clear ./
|
||||
|
||||
.PHONY: clean-db
|
||||
clean-db: ## 删除migrations文件夹和db.sqlite3
|
||||
find . -type d -name "migrations" -exec rm -rf {} +
|
||||
rm -f db.sqlite3 db.sqlite3-shm db.sqlite3-wal
|
||||
|
||||
.PHONY: migrate
|
||||
migrate: ## 运行aerich migrate命令生成迁移文件
|
||||
aerich migrate
|
||||
|
||||
.PHONY: upgrade
|
||||
upgrade: ## 运行aerich upgrade命令应用迁移
|
||||
aerich upgrade
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/mizhexiaoxiao/vue-fastapi-admin">
|
||||
<img alt="Vue FastAPI Admin Logo" width="200" src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/logo.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">vue-fastapi-admin</h1>
|
||||
|
||||
English | [简体中文](./README.md)
|
||||
|
||||
vue-fastapi-admin is a modern front-end and back-end separation development platform that combines FastAPI, Vue3, and Naive UI. It incorporates RBAC (Role-Based Access Control) management, dynamic routing, and JWT (JSON Web Token) authentication, making it ideal for rapid development of small to medium-sized applications and also serves as a valuable learning resource.
|
||||
|
||||
### Features
|
||||
- **Popular Tech Stack**: The backend is developed with the high-performance asynchronous framework FastAPI using Python 3.11, while the front-end is powered by cutting-edge technologies such as Vue3 and Vite, complemented by the efficient package manager, pnpm.
|
||||
- **Code Standards**: The project is equipped with various plugins for code standardization and quality control, ensuring consistency and enhancing team collaboration efficiency.
|
||||
- **Dynamic Routing**: Backend dynamic routing combined with the RBAC model allows for fine-grained control of menus and routing.
|
||||
- **JWT Authentication**: User identity verification and authorization are handled through JWT, enhancing the application's security.
|
||||
- **Granular Permission Control**: Implements detailed permission management including button and interface level controls, ensuring different roles and users have appropriate permissions.
|
||||
|
||||
### Live Demo
|
||||
- URL: http://139.9.100.77:9999
|
||||
- Username: admin
|
||||
- Password: 123456
|
||||
|
||||
### Screenshots
|
||||
|
||||
#### Login Page
|
||||

|
||||
|
||||
#### Workbench
|
||||

|
||||
|
||||
#### User Management
|
||||

|
||||
|
||||
#### Role Management
|
||||

|
||||
|
||||
#### Menu Management
|
||||

|
||||
|
||||
#### API Management
|
||||

|
||||
|
||||
### Quick Start
|
||||
Please follow the instructions below for installation and configuration:
|
||||
|
||||
#### Method 1:dockerhub pull image
|
||||
|
||||
```sh
|
||||
docker pull mizhexiaoxiao/vue-fastapi-admin:latest
|
||||
docker run -d --restart=always --name=vue-fastapi-admin -p 9999:80 mizhexiaoxiao/vue-fastapi-admin
|
||||
```
|
||||
|
||||
#### Method 2: Build Image Using Dockerfile
|
||||
##### Install Docker
|
||||
|
||||
```sh
|
||||
yum install -y docker-ce
|
||||
systemctl start docker
|
||||
```
|
||||
|
||||
##### Build the Image
|
||||
|
||||
```sh
|
||||
git clone https://github.com/mizhexiaoxiao/vue-fastapi-admin.git
|
||||
cd vue-fastapi-admin
|
||||
docker build --no-cache . -t vue-fastapi-admin
|
||||
```
|
||||
|
||||
##### Start the Container
|
||||
|
||||
```sh
|
||||
docker run -d --restart=always --name=vue-fastapi-admin -p 9999:80 vue-fastapi-admin
|
||||
```
|
||||
|
||||
##### Access the Service
|
||||
|
||||
http://localhost:9999
|
||||
|
||||
username:admin
|
||||
|
||||
password:123456
|
||||
|
||||
### Local Setup
|
||||
#### Backend
|
||||
The backend service requires the following environment:
|
||||
- Python 3.11
|
||||
|
||||
#### Method 1 (Recommended): Install Dependencies with uv
|
||||
1. Install uv
|
||||
```sh
|
||||
pip install uv
|
||||
```
|
||||
|
||||
2. Create and activate virtual environment
|
||||
```sh
|
||||
uv venv
|
||||
source .venv/bin/activate # Linux/Mac
|
||||
# or
|
||||
.\.venv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
3. Install dependencies
|
||||
```sh
|
||||
uv add pyproject.toml
|
||||
```
|
||||
|
||||
4. Start the backend service
|
||||
```sh
|
||||
python run.py
|
||||
```
|
||||
|
||||
#### Method 2: Install Dependencies with Pip
|
||||
1. Create a Python virtual environment:
|
||||
```sh
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate # Linux/Mac
|
||||
# or
|
||||
.\venv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
2. Install project dependencies:
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Start the backend service:
|
||||
```sh
|
||||
python run.py
|
||||
```
|
||||
The backend service is now running, and you can visit http://localhost:9999/docs to view the API documentation.
|
||||
|
||||
#### Frontend
|
||||
The frontend project requires a Node.js environment (recommended version 18.8.0 or higher).
|
||||
- node v18.8.0+
|
||||
|
||||
1. Navigate to the frontend project directory:
|
||||
```sh
|
||||
cd web
|
||||
```
|
||||
|
||||
2. Install project dependencies (pnpm is recommended: https://pnpm.io/zh/installation)
|
||||
```sh
|
||||
npm i -g pnpm # If pnpm is already installed, skip this step
|
||||
pnpm i # Or use npm i
|
||||
```
|
||||
|
||||
3. Start the frontend development server:
|
||||
```sh
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Directory Structure Explanation
|
||||
|
||||
```
|
||||
├── app // Application directory
|
||||
│ ├── api // API interface directory
|
||||
│ │ └── v1 // Version 1 of the API interfaces
|
||||
│ │ ├── apis // API-related interfaces
|
||||
│ │ ├── base // Base information interfaces
|
||||
│ │ ├── menus // Menu related interfaces
|
||||
│ │ ├── roles // Role related interfaces
|
||||
│ │ └── users // User related interfaces
|
||||
│ ├── controllers // Controllers directory
|
||||
│ ├── core // Core functionality module
|
||||
│ ├── log // Log directory
|
||||
│ ├── models // Data models directory
|
||||
│ ├── schemas // Data schema/structure definitions
|
||||
│ ├── settings // Configuration settings directory
|
||||
│ └── utils // Utilities directory
|
||||
├── deploy // Deployment related directory
|
||||
│ └── sample-picture // Sample picture directory
|
||||
└── web // Front-end web directory
|
||||
├── build // Build scripts and configuration directory
|
||||
│ ├── config // Build configurations
|
||||
│ ├── plugin // Build plugins
|
||||
│ └── script // Build scripts
|
||||
├── public // Public resources directory
|
||||
│ └── resource // Public resource files
|
||||
├── settings // Front-end project settings
|
||||
└── src // Source code directory
|
||||
├── api // API interface definitions
|
||||
├── assets // Static resources directory
|
||||
│ ├── images // Image resources
|
||||
│ ├── js // JavaScript files
|
||||
│ └── svg // SVG vector files
|
||||
├── components // Components directory
|
||||
│ ├── common // Common components
|
||||
│ ├── icon // Icon components
|
||||
│ ├── page // Page components
|
||||
│ ├── query-bar // Query bar components
|
||||
│ └── table // Table components
|
||||
├── composables // Composable functionalities
|
||||
├── directives // Directives directory
|
||||
├── layout // Layout directory
|
||||
│ └── components // Layout components
|
||||
├── router // Routing directory
|
||||
│ ├── guard // Route guards
|
||||
│ └── routes // Route definitions
|
||||
├── store // State management (pinia)
|
||||
│ └── modules // State modules
|
||||
├── styles // Style files directory
|
||||
├── utils // Utilities directory
|
||||
│ ├── auth // Authentication related utilities
|
||||
│ ├── common // Common utilities
|
||||
│ ├── http // Encapsulated axios
|
||||
│ └── storage // Encapsulated localStorage and sessionStorage
|
||||
└── views // Views/Pages directory
|
||||
├── error-page // Error pages
|
||||
├── login // Login page
|
||||
├── profile // Profile page
|
||||
├── system // System management page
|
||||
└── workbench // Workbench page
|
||||
```
|
||||
|
||||
### Visitors Count
|
||||
|
||||
<img align="left" src = "https://profile-counter.glitch.me/vue-fastapi-admin/count.svg" alt="Loading">
|
||||
@@ -0,0 +1,244 @@
|
||||
<p align="center">
|
||||
<a href="https://gitee.com/mizhexiaoxiao/vue-fastapi-admin">
|
||||
<img alt="Vue FastAPI Admin Logo" width="200" src="https://gitee.com/mizhexiaoxiao/vue-fastapi-admin/raw/main/deploy/sample-picture/logo.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">vue-fastapi-admin</h1>
|
||||
|
||||
[English](./README-en.md) | 简体中文
|
||||
|
||||
基于 FastAPI + Vue3 + Naive UI 的现代化前后端分离开发平台,融合了 RBAC 权限管理、动态路由和 JWT 鉴权,助力中小型应用快速搭建,也可用于学习参考。
|
||||
|
||||
### 特性
|
||||
- **最流行技术栈**:基于 Python 3.11 和 FastAPI 高性能异步框架,结合 Vue3 和 Vite 等前沿技术进行开发,同时使用高效的 npm 包管理器 pnpm。
|
||||
- **代码规范**:项目内置丰富的规范插件,确保代码质量和一致性,有效提高团队协作效率。
|
||||
- **动态路由**:后端动态路由,结合 RBAC(Role-Based Access Control)权限模型,提供精细的菜单路由控制。
|
||||
- **JWT鉴权**:使用 JSON Web Token(JWT)进行身份验证和授权,增强应用的安全性。
|
||||
- **细粒度权限控制**:实现按钮和接口级别的权限控制,确保不同用户或角色在界面操作和接口访问时具有不同的权限限制。
|
||||
|
||||
### 在线预览
|
||||
- http://47.111.145.81:3000
|
||||
- username: admin
|
||||
- password: 123456
|
||||
|
||||
### 登录页
|
||||
|
||||

|
||||
### 工作台
|
||||
|
||||

|
||||
|
||||
### 用户管理
|
||||
|
||||

|
||||
### 角色管理
|
||||
|
||||

|
||||
|
||||
### 菜单管理
|
||||
|
||||

|
||||
|
||||
### API管理
|
||||
|
||||

|
||||
|
||||
### 快速开始
|
||||
#### 方法一:dockerhub拉取镜像
|
||||
|
||||
```sh
|
||||
docker pull mizhexiaoxiao/vue-fastapi-admin:latest
|
||||
docker run -d --restart=always --name=vue-fastapi-admin -p 9999:80 mizhexiaoxiao/vue-fastapi-admin
|
||||
```
|
||||
|
||||
#### 方法二:dockerfile构建镜像
|
||||
##### docker安装(版本17.05+)
|
||||
|
||||
```sh
|
||||
yum install -y docker-ce
|
||||
systemctl start docker
|
||||
```
|
||||
|
||||
##### 构建镜像
|
||||
|
||||
```sh
|
||||
git clone https://gitee.com/mizhexiaoxiao/vue-fastapi-admin.git
|
||||
cd vue-fastapi-admin
|
||||
docker build --no-cache . -t vue-fastapi-admin
|
||||
```
|
||||
|
||||
##### 启动容器
|
||||
|
||||
```sh
|
||||
docker run -d --restart=always --name=vue-fastapi-admin -p 9999:80 vue-fastapi-admin
|
||||
```
|
||||
|
||||
##### 访问
|
||||
|
||||
http://localhost:9999
|
||||
|
||||
username:admin
|
||||
|
||||
password:123456
|
||||
|
||||
### 本地启动
|
||||
#### 后端
|
||||
启动项目需要以下环境:
|
||||
- Python 3.11
|
||||
|
||||
#### 方法一(推荐):使用 uv 安装依赖
|
||||
1. 安装 uv
|
||||
```sh
|
||||
pip install uv
|
||||
```
|
||||
|
||||
2. 创建并激活虚拟环境
|
||||
```sh
|
||||
uv venv
|
||||
source .venv/bin/activate # Linux/Mac
|
||||
# 或
|
||||
.\.venv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
3. 安装依赖
|
||||
```sh
|
||||
uv add pyproject.toml
|
||||
```
|
||||
|
||||
4. 启动服务
|
||||
```sh
|
||||
python run.py
|
||||
```
|
||||
|
||||
#### 方法二:使用 Pip 安装依赖
|
||||
1. 创建虚拟环境
|
||||
```sh
|
||||
python3 -m venv venv
|
||||
```
|
||||
|
||||
2. 激活虚拟环境
|
||||
```sh
|
||||
source venv/bin/activate # Linux/Mac
|
||||
# 或
|
||||
.\venv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
3. 安装依赖
|
||||
```sh
|
||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
```
|
||||
|
||||
4. 启动服务
|
||||
```sh
|
||||
python run.py
|
||||
```
|
||||
|
||||
服务现在应该正在运行,访问 http://localhost:9999/docs 查看API文档
|
||||
|
||||
#### 前端
|
||||
启动项目需要以下环境:
|
||||
- node v18.8.0+
|
||||
|
||||
1. 进入前端目录
|
||||
```sh
|
||||
cd web
|
||||
```
|
||||
|
||||
2. 安装依赖(建议使用pnpm: https://pnpm.io/zh/installation)
|
||||
```sh
|
||||
npm i -g pnpm # 已安装可忽略
|
||||
pnpm i # 或者 npm i
|
||||
```
|
||||
|
||||
3. 启动
|
||||
```sh
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 目录说明
|
||||
|
||||
```
|
||||
├── app // 应用程序目录
|
||||
│ ├── api // API接口目录
|
||||
│ │ └── v1 // 版本1的API接口
|
||||
│ │ ├── apis // API相关接口
|
||||
│ │ ├── base // 基础信息接口
|
||||
│ │ ├── menus // 菜单相关接口
|
||||
│ │ ├── roles // 角色相关接口
|
||||
│ │ └── users // 用户相关接口
|
||||
│ ├── controllers // 控制器目录
|
||||
│ ├── core // 核心功能模块
|
||||
│ ├── log // 日志目录
|
||||
│ ├── models // 数据模型目录
|
||||
│ ├── schemas // 数据模式/结构定义
|
||||
│ ├── settings // 配置设置目录
|
||||
│ └── utils // 工具类目录
|
||||
├── deploy // 部署相关目录
|
||||
│ └── sample-picture // 示例图片目录
|
||||
└── web // 前端网页目录
|
||||
├── build // 构建脚本和配置目录
|
||||
│ ├── config // 构建配置
|
||||
│ ├── plugin // 构建插件
|
||||
│ └── script // 构建脚本
|
||||
├── public // 公共资源目录
|
||||
│ └── resource // 公共资源文件
|
||||
├── settings // 前端项目配置
|
||||
└── src // 源代码目录
|
||||
├── api // API接口定义
|
||||
├── assets // 静态资源目录
|
||||
│ ├── images // 图片资源
|
||||
│ ├── js // JavaScript文件
|
||||
│ └── svg // SVG矢量图文件
|
||||
├── components // 组件目录
|
||||
│ ├── common // 通用组件
|
||||
│ ├── icon // 图标组件
|
||||
│ ├── page // 页面组件
|
||||
│ ├── query-bar // 查询栏组件
|
||||
│ └── table // 表格组件
|
||||
├── composables // 可组合式功能块
|
||||
├── directives // 指令目录
|
||||
├── layout // 布局目录
|
||||
│ └── components // 布局组件
|
||||
├── router // 路由目录
|
||||
│ ├── guard // 路由守卫
|
||||
│ └── routes // 路由定义
|
||||
├── store // 状态管理(pinia)
|
||||
│ └── modules // 状态模块
|
||||
├── styles // 样式文件目录
|
||||
├── utils // 工具类目录
|
||||
│ ├── auth // 认证相关工具
|
||||
│ ├── common // 通用工具
|
||||
│ ├── http // 封装axios
|
||||
│ └── storage // 封装localStorage和sessionStorage
|
||||
└── views // 视图/页面目录
|
||||
├── error-page // 错误页面
|
||||
├── login // 登录页面
|
||||
├── profile // 个人资料页面
|
||||
├── system // 系统管理页面
|
||||
└── workbench // 工作台页面
|
||||
```
|
||||
|
||||
### 进群交流
|
||||
进群的条件是给项目一个star,小小的star是作者维护下去的动力。
|
||||
|
||||
你可以在群里提出任何疑问,我会尽快回复答疑。
|
||||
|
||||
<img width="300" src="https://gitee.com/mizhexiaoxiao/vue-fastapi-admin/raw/main/deploy/sample-picture/group.jpg">
|
||||
|
||||
## 打赏
|
||||
如果项目有帮助到你,可以请作者喝杯咖啡~
|
||||
|
||||
<div style="display: flex">
|
||||
<img src="https://gitee.com/mizhexiaoxiao/vue-fastapi-admin/raw/main/deploy/sample-picture/1.jpg" width="300">
|
||||
<img src="https://gitee.com/mizhexiaoxiao/vue-fastapi-admin/raw/main/deploy/sample-picture/2.jpg" width="300">
|
||||
</div>
|
||||
|
||||
## 定制开发
|
||||
如果有基于该项目的定制需求或其他合作,请添加下方微信,备注来意
|
||||
|
||||
<img width="300" src="https://gitee.com/mizhexiaoxiao/vue-fastapi-admin/raw/main/deploy/sample-picture/3.jpg">
|
||||
|
||||
### Visitors Count
|
||||
|
||||
<img align="left" src = "https://profile-counter.glitch.me/vue-fastapi-admin/count.svg" alt="Loading">
|
||||
@@ -0,0 +1,49 @@
|
||||
from pathlib import Path
|
||||
import sys; sys.path.extend([str(Path(__file__).parent / 'utils' / 'some_sdk')])
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from tortoise import Tortoise
|
||||
|
||||
from app.core.exceptions import SettingNotFound
|
||||
from app.core.init_app import (
|
||||
init_data,
|
||||
tear_down,
|
||||
make_middlewares,
|
||||
register_exceptions,
|
||||
register_routers,
|
||||
mount_static_and_config_swagger
|
||||
)
|
||||
|
||||
try:
|
||||
from app.settings.config import settings
|
||||
except ImportError:
|
||||
raise SettingNotFound("Can not import settings")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await init_data()
|
||||
yield
|
||||
await Tortoise.close_connections()
|
||||
await tear_down()
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
app = FastAPI(
|
||||
title=settings.APP_TITLE,
|
||||
description=settings.APP_DESCRIPTION,
|
||||
version=settings.VERSION,
|
||||
openapi_url="/api/openapi.json",
|
||||
docs_url="/api/api-docs",
|
||||
middleware=make_middlewares(),
|
||||
lifespan=lifespan,
|
||||
)
|
||||
register_exceptions(app)
|
||||
register_routers(app, prefix="/api")
|
||||
mount_static_and_config_swagger(app)
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
@@ -0,0 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .v1 import v1_router
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(v1_router, prefix="/v1")
|
||||
|
||||
|
||||
__all__ = ["api_router"]
|
||||
@@ -0,0 +1,29 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.core.dependency import DependPermisson, DependWeixinUser
|
||||
|
||||
from .apis import apis_router
|
||||
from .auditlog import auditlog_router
|
||||
from .base import base_router
|
||||
from .depts import depts_router
|
||||
from .menus import menus_router
|
||||
from .roles import roles_router
|
||||
from .users import users_router
|
||||
from .weixin import weixin_router
|
||||
from .finance_parse import finance_parse_router
|
||||
from .msg import msg_route_router
|
||||
from .automation import automation_router
|
||||
|
||||
v1_router = APIRouter()
|
||||
|
||||
v1_router.include_router(base_router, prefix="/base")
|
||||
v1_router.include_router(users_router, prefix="/user", dependencies=[DependPermisson])
|
||||
v1_router.include_router(roles_router, prefix="/role", dependencies=[DependPermisson])
|
||||
v1_router.include_router(menus_router, prefix="/menu", dependencies=[DependPermisson])
|
||||
v1_router.include_router(apis_router, prefix="/api", dependencies=[DependPermisson])
|
||||
v1_router.include_router(depts_router, prefix="/dept", dependencies=[DependPermisson])
|
||||
v1_router.include_router(auditlog_router, prefix="/auditlog", dependencies=[DependPermisson])
|
||||
v1_router.include_router(finance_parse_router, prefix="/parse_finance_data", dependencies=[DependPermisson])
|
||||
v1_router.include_router(weixin_router, prefix='/weixin')
|
||||
v1_router.include_router(automation_router, prefix='/auto')
|
||||
v1_router.include_router(msg_route_router, dependencies=[DependWeixinUser])
|
||||
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .apis import router
|
||||
|
||||
apis_router = APIRouter()
|
||||
apis_router.include_router(router, tags=["API模块"])
|
||||
|
||||
__all__ = ["apis_router"]
|
||||
@@ -0,0 +1,67 @@
|
||||
from fastapi import APIRouter, Query
|
||||
from tortoise.expressions import Q
|
||||
|
||||
from app.controllers.api import api_controller
|
||||
from app.schemas import Success, SuccessExtra
|
||||
from app.schemas.apis import *
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/list", summary="查看API列表")
|
||||
async def list_api(
|
||||
page: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
path: str = Query(None, description="API路径"),
|
||||
summary: str = Query(None, description="API简介"),
|
||||
tags: str = Query(None, description="API模块"),
|
||||
):
|
||||
q = Q()
|
||||
if path:
|
||||
q &= Q(path__contains=path)
|
||||
if summary:
|
||||
q &= Q(summary__contains=summary)
|
||||
if tags:
|
||||
q &= Q(tags__contains=tags)
|
||||
total, api_objs = await api_controller.list(page=page, page_size=page_size, search=q, order=["tags", "id"])
|
||||
data = [await obj.to_dict() for obj in api_objs]
|
||||
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get("/get", summary="查看Api")
|
||||
async def get_api(
|
||||
id: int = Query(..., description="Api"),
|
||||
):
|
||||
api_obj = await api_controller.get(id=id)
|
||||
data = await api_obj.to_dict()
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
@router.post("/create", summary="创建Api")
|
||||
async def create_api(
|
||||
api_in: ApiCreate,
|
||||
):
|
||||
await api_controller.create(obj_in=api_in)
|
||||
return Success(msg="Created Successfully")
|
||||
|
||||
|
||||
@router.post("/update", summary="更新Api")
|
||||
async def update_api(
|
||||
api_in: ApiUpdate,
|
||||
):
|
||||
await api_controller.update(id=api_in.id, obj_in=api_in)
|
||||
return Success(msg="Update Successfully")
|
||||
|
||||
|
||||
@router.delete("/delete", summary="删除Api")
|
||||
async def delete_api(
|
||||
api_id: int = Query(..., description="ApiID"),
|
||||
):
|
||||
await api_controller.remove(id=api_id)
|
||||
return Success(msg="Deleted Success")
|
||||
|
||||
|
||||
@router.post("/refresh", summary="刷新API列表")
|
||||
async def refresh_api():
|
||||
await api_controller.refresh_api()
|
||||
return Success(msg="OK")
|
||||
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .auditlog import router
|
||||
|
||||
auditlog_router = APIRouter()
|
||||
auditlog_router.include_router(router, tags=["审计日志模块"])
|
||||
|
||||
__all__ = ["auditlog_router"]
|
||||
@@ -0,0 +1,44 @@
|
||||
from fastapi import APIRouter, Query
|
||||
from tortoise.expressions import Q
|
||||
|
||||
from app.models.admin import AuditLog
|
||||
from app.schemas import SuccessExtra
|
||||
from app.schemas.apis import *
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/list", summary="查看操作日志")
|
||||
async def get_audit_log_list(
|
||||
page: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
username: str = Query("", description="操作人名称"),
|
||||
module: str = Query("", description="功能模块"),
|
||||
method: str = Query("", description="请求方法"),
|
||||
summary: str = Query("", description="接口描述"),
|
||||
status: int = Query(None, description="状态码"),
|
||||
start_time: str = Query("", description="开始时间"),
|
||||
end_time: str = Query("", description="结束时间"),
|
||||
):
|
||||
q = Q()
|
||||
if username:
|
||||
q &= Q(username__icontains=username)
|
||||
if module:
|
||||
q &= Q(module__icontains=module)
|
||||
if method:
|
||||
q &= Q(method__icontains=method)
|
||||
if summary:
|
||||
q &= Q(summary__icontains=summary)
|
||||
if status:
|
||||
q &= Q(status=status)
|
||||
if start_time and end_time:
|
||||
q &= Q(created_at__range=[start_time, end_time])
|
||||
elif start_time:
|
||||
q &= Q(created_at__gte=start_time)
|
||||
elif end_time:
|
||||
q &= Q(created_at__lte=end_time)
|
||||
|
||||
audit_log_objs = await AuditLog.filter(q).offset((page - 1) * page_size).limit(page_size).order_by("-created_at")
|
||||
total = await AuditLog.filter(q).count()
|
||||
data = [await audit_log.to_dict() for audit_log in audit_log_objs]
|
||||
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
|
||||
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .automation import router
|
||||
|
||||
automation_router = APIRouter()
|
||||
automation_router.include_router(router, tags=["自动化模块"])
|
||||
|
||||
__all__ = ["automation_router"]
|
||||
@@ -0,0 +1,188 @@
|
||||
# app/api/v1/automation.py
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
from app.http_base import unified_resp
|
||||
from fastapi import APIRouter, Query, Depends
|
||||
from tortoise.expressions import Q
|
||||
from app.controllers.automation.scenario import automation_scenario_controller
|
||||
from app.controllers.automation.task import task_controller
|
||||
from app.controllers.automation.action import action_controller
|
||||
from app.schemas.automation import (
|
||||
ScenarioCreate,
|
||||
ScenarioUpdate,
|
||||
ScenarioCopy,
|
||||
TaskUpdate,
|
||||
ActionUpdate,
|
||||
)
|
||||
from app.schemas.base import Success, SuccessExtra, Fail
|
||||
from app.schemas.apis import Paginate
|
||||
from ..weixin.base import get_weixin_user
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============================
|
||||
# Scenario 路由
|
||||
# ============================
|
||||
|
||||
@router.get("/scenario/list", summary="查看场景列表")
|
||||
@unified_resp
|
||||
async def list_automation_scenario(
|
||||
pagination: Paginate = Depends(),
|
||||
title: str = Query("", description="场景标题,用于搜索"),
|
||||
is_global: bool = Query(None, description="是否全局场景"),
|
||||
enabled: bool = Query(None, description="是否启用"),
|
||||
owner_user_id: str = Query("", description="归属用户ID(非全局时)"),
|
||||
):
|
||||
q = Q(visible=True)
|
||||
if title:
|
||||
q &= Q(title__contains=title)
|
||||
if is_global is not None:
|
||||
q &= Q(is_global=is_global)
|
||||
if enabled is not None:
|
||||
q &= Q(enabled=enabled)
|
||||
if owner_user_id:
|
||||
q &= Q(owner_user_id=owner_user_id)
|
||||
|
||||
total, scenario_objs = await automation_scenario_controller.list(
|
||||
page=pagination.page, page_size=pagination.page_size, search=q, order=["-created_at"]
|
||||
)
|
||||
data = [await obj.to_dict() for obj in scenario_objs]
|
||||
return {"count": total, "lists": data, "page": pagination.page, "page_size": pagination.page_size}
|
||||
|
||||
|
||||
@router.get("/scenario/event_and_action", summary="查看场景事件和动作")
|
||||
@unified_resp
|
||||
async def list_event_and_action():
|
||||
actions = await action_controller.list_automation_actions()
|
||||
events = await automation_scenario_controller.list_automation_events()
|
||||
return {"events": events, "actions": actions}
|
||||
|
||||
|
||||
@router.get("/scenario/get/{id}", summary="查看场景详情")
|
||||
@unified_resp
|
||||
async def get_automation_scenario(id: int):
|
||||
obj = await automation_scenario_controller.get(id=id)
|
||||
return await obj.to_dict()
|
||||
|
||||
|
||||
@router.post("/scenario/create", summary="创建场景")
|
||||
@unified_resp
|
||||
async def create_automation_scenario(
|
||||
scenario_in: ScenarioCreate,
|
||||
weixin_user: dict = Depends(get_weixin_user)
|
||||
):
|
||||
obj = await automation_scenario_controller.new_scenario(scenario_in, weixin_user.userid)
|
||||
return await obj.to_dict()
|
||||
|
||||
|
||||
@router.post("/scenario/copy", summary="复制场景")
|
||||
@unified_resp
|
||||
async def copy_automation_scenario(
|
||||
scenario_in: ScenarioCopy,
|
||||
):
|
||||
def handler(obj_dict: Dict[str, Any]) -> Dict[str, Any]:
|
||||
obj_dict["title"] = f"复制:{obj_dict['title']}"
|
||||
obj_dict["enabled"] = False
|
||||
return obj_dict
|
||||
|
||||
obj = await automation_scenario_controller.copy(id=scenario_in.id, handler=handler)
|
||||
return await obj.to_dict()
|
||||
|
||||
|
||||
@router.post("/scenario/update", summary="更新场景")
|
||||
@unified_resp
|
||||
async def update_automation_scenario(
|
||||
scenario_in: ScenarioUpdate,
|
||||
weixin_user: dict = Depends(get_weixin_user)
|
||||
):
|
||||
obj = await automation_scenario_controller.update_scenario(scenario_in=scenario_in, user_id=weixin_user.userid)
|
||||
return await obj.to_dict()
|
||||
|
||||
|
||||
@router.delete("/scenario/delete", summary="删除场景")
|
||||
@unified_resp
|
||||
async def delete_automation_scenario(
|
||||
id: int = Query(..., description="场景ID"),
|
||||
):
|
||||
await automation_scenario_controller.remove_scenario(scenario_id=id)
|
||||
return Success(msg="Deleted Successfully")
|
||||
|
||||
|
||||
# ============================
|
||||
# Task 路由
|
||||
# ============================
|
||||
|
||||
@router.get("/task/list", summary="查看待办列表")
|
||||
@unified_resp
|
||||
async def list_task(
|
||||
pagination: Paginate = Depends(),
|
||||
assignee_user_id: str = Query("", description="指派人用户ID"),
|
||||
status: str = Query("", description="任务状态"),
|
||||
related_customer_id: str = Query("", description="关联客户ID"),
|
||||
):
|
||||
q = Q()
|
||||
if assignee_user_id:
|
||||
q &= Q(assignee_user_id=assignee_user_id)
|
||||
if status:
|
||||
q &= Q(status=status)
|
||||
if related_customer_id:
|
||||
q &= Q(related_customer_id=related_customer_id)
|
||||
|
||||
total, task_objs = await task_controller.list(
|
||||
page=pagination.page, page_size=pagination.page_size, search=q
|
||||
# , order=["-created_at"]
|
||||
)
|
||||
data = [await obj.to_dict() for obj in task_objs]
|
||||
return {"count": total, "lists": data, "page": pagination.page, "page_size": pagination.page_size}
|
||||
|
||||
|
||||
@router.get("/task/get", summary="查看待办详情")
|
||||
@unified_resp
|
||||
async def get_task(
|
||||
id: int = Query(..., description="待办ID"),
|
||||
):
|
||||
obj = await task_controller.get(id=id)
|
||||
return await obj.to_dict()
|
||||
|
||||
|
||||
@router.post("/task/update", summary="更新待办")
|
||||
@unified_resp
|
||||
async def update_task(
|
||||
task_in: TaskUpdate,
|
||||
):
|
||||
obj = await task_controller.update(id=task_in.id, obj_in=task_in)
|
||||
return await obj.to_dict()
|
||||
|
||||
|
||||
# ============================
|
||||
# Action 路由
|
||||
# ============================
|
||||
|
||||
@router.get("/action/list", summary="查看动作列表(按任务)")
|
||||
@unified_resp
|
||||
async def list_action(
|
||||
task_id: int = Query(..., description="所属任务ID"),
|
||||
):
|
||||
actions = await action_controller.model.filter(task_id=task_id).all()
|
||||
data = [await act.to_dict() for act in actions]
|
||||
return data
|
||||
|
||||
|
||||
# @router.get("/action/type", summary="查看动作类型列表(按任务)")
|
||||
# @unified_resp
|
||||
# async def list_action_type():
|
||||
# actions = await action_controller.list_automation_actions()
|
||||
# return actions
|
||||
|
||||
|
||||
@router.post("/action/update", summary="更新动作(如标记完成)")
|
||||
@unified_resp
|
||||
async def update_action(
|
||||
action_in: ActionUpdate,
|
||||
):
|
||||
obj = await action_controller.update(id=action_in.id, obj_in=action_in)
|
||||
return await obj.to_dict()
|
||||
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .base import router
|
||||
|
||||
base_router = APIRouter()
|
||||
base_router.include_router(router, tags=["基础模块"])
|
||||
|
||||
__all__ = ["base_router"]
|
||||
@@ -0,0 +1,103 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.controllers.user import user_controller
|
||||
from app.core.ctx import CTX_USER_ID
|
||||
from app.core.dependency import DependAuth
|
||||
from app.models.admin import Api, Menu, Role, User
|
||||
from app.schemas.base import Fail, Success
|
||||
from app.schemas.login import *
|
||||
from app.schemas.users import UpdatePassword
|
||||
from app.settings import settings
|
||||
from app.utils.jwt import create_access_token
|
||||
from app.utils.password import get_password_hash, verify_password
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/access_token", summary="获取token")
|
||||
async def login_access_token(credentials: CredentialsSchema):
|
||||
user: User = await user_controller.authenticate(credentials)
|
||||
await user_controller.update_last_login(user.id)
|
||||
access_token_expires = timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
expire = datetime.now(timezone.utc) + access_token_expires
|
||||
|
||||
data = JWTOut(
|
||||
access_token=create_access_token(
|
||||
data=JWTPayload(
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
is_superuser=user.is_superuser,
|
||||
exp=expire,
|
||||
)
|
||||
),
|
||||
username=user.username,
|
||||
)
|
||||
return Success(data=data.model_dump())
|
||||
|
||||
|
||||
@router.get("/userinfo", summary="查看用户信息", dependencies=[DependAuth])
|
||||
async def get_userinfo():
|
||||
user_id = CTX_USER_ID.get()
|
||||
user_obj = await user_controller.get(id=user_id)
|
||||
data = await user_obj.to_dict(exclude_fields=["password"])
|
||||
data["avatar"] = "https://avatars.githubusercontent.com/u/54677442?v=4"
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
@router.get("/usermenu", summary="查看用户菜单", dependencies=[DependAuth])
|
||||
async def get_user_menu():
|
||||
user_id = CTX_USER_ID.get()
|
||||
user_obj = await User.filter(id=user_id).first()
|
||||
menus: list[Menu] = []
|
||||
if user_obj.is_superuser:
|
||||
menus = await Menu.all()
|
||||
else:
|
||||
role_objs: list[Role] = await user_obj.roles
|
||||
for role_obj in role_objs:
|
||||
menu = await role_obj.menus
|
||||
menus.extend(menu)
|
||||
menus = list(set(menus))
|
||||
parent_menus: list[Menu] = []
|
||||
for menu in menus:
|
||||
if menu.parent_id == 0:
|
||||
parent_menus.append(menu)
|
||||
res = []
|
||||
for parent_menu in parent_menus:
|
||||
parent_menu_dict = await parent_menu.to_dict()
|
||||
parent_menu_dict["children"] = []
|
||||
for menu in menus:
|
||||
if menu.parent_id == parent_menu.id:
|
||||
parent_menu_dict["children"].append(await menu.to_dict())
|
||||
res.append(parent_menu_dict)
|
||||
return Success(data=res)
|
||||
|
||||
|
||||
@router.get("/userapi", summary="查看用户API", dependencies=[DependAuth])
|
||||
async def get_user_api():
|
||||
user_id = CTX_USER_ID.get()
|
||||
user_obj = await User.filter(id=user_id).first()
|
||||
if user_obj.is_superuser:
|
||||
api_objs: list[Api] = await Api.all()
|
||||
apis = [api.method.lower() + api.path for api in api_objs]
|
||||
return Success(data=apis)
|
||||
role_objs: list[Role] = await user_obj.roles
|
||||
apis = []
|
||||
for role_obj in role_objs:
|
||||
api_objs: list[Api] = await role_obj.apis
|
||||
apis.extend([api.method.lower() + api.path for api in api_objs])
|
||||
apis = list(set(apis))
|
||||
return Success(data=apis)
|
||||
|
||||
|
||||
@router.post("/update_password", summary="修改密码", dependencies=[DependAuth])
|
||||
async def update_user_password(req_in: UpdatePassword):
|
||||
user_id = CTX_USER_ID.get()
|
||||
user = await user_controller.get(user_id)
|
||||
verified = verify_password(req_in.old_password, user.password)
|
||||
if not verified:
|
||||
return Fail(msg="旧密码验证错误!")
|
||||
user.password = get_password_hash(req_in.new_password)
|
||||
await user.save()
|
||||
return Success(msg="修改成功")
|
||||
@@ -0,0 +1,10 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .table import router
|
||||
from .datasource import router as datasource_router
|
||||
|
||||
codegen_router = APIRouter()
|
||||
codegen_router.include_router(router, tags=["代码生成模块"])
|
||||
codegen_router.include_router(datasource_router, tags=["数据源管理"])
|
||||
|
||||
__all__ = ["codegen_router"]
|
||||
@@ -0,0 +1,73 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
from fastapi.exceptions import HTTPException
|
||||
from tortoise.expressions import Q
|
||||
|
||||
from app.controllers.datasource import datasource_controller
|
||||
from app.schemas.base import Success, SuccessExtra
|
||||
from app.schemas.roles import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/datasource")
|
||||
|
||||
|
||||
@router.get("/list", summary="查看模板列表")
|
||||
async def list_role(
|
||||
page: int = Query(1, description="页码"),
|
||||
limit: int = Query(10, description="每页数量"),
|
||||
table_name: str = Query("", description="名称,用于查询"),
|
||||
):
|
||||
# data = await datasource_controller.load_tables()
|
||||
# return SuccessExtra(data=data)
|
||||
q = Q()
|
||||
if table_name:
|
||||
q = Q(name__contains=table_name)
|
||||
total, role_objs = await datasource_controller.list(page=page, page_size=limit, search=q)
|
||||
data = [await obj.to_dict() for obj in role_objs]
|
||||
return SuccessExtra(data=data, total=total, page=page, page_size=limit)
|
||||
|
||||
|
||||
@router.get("/get/{table_name}", summary="查看")
|
||||
async def get_role(table_name):
|
||||
data = await datasource_controller.load_tables(name=table_name)
|
||||
return SuccessExtra(data=data)
|
||||
|
||||
|
||||
@router.post("/create", summary="创建")
|
||||
async def create_role(role_in: RoleCreate):
|
||||
if await datasource_controller.is_exist(name=role_in.name):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="The role with this rolename already exists in the system.",
|
||||
)
|
||||
await datasource_controller.create(obj_in=role_in)
|
||||
return Success(msg="Created Successfully")
|
||||
|
||||
|
||||
@router.post("/update", summary="更新")
|
||||
async def update_role(role_in: RoleUpdate):
|
||||
await datasource_controller.update(id=role_in.id, obj_in=role_in)
|
||||
return Success(msg="Updated Successfully")
|
||||
|
||||
|
||||
@router.delete("/delete", summary="删除")
|
||||
async def delete_role(
|
||||
role_id: int = Query(..., description="ID"),
|
||||
):
|
||||
await datasource_controller.remove(id=role_id)
|
||||
return Success(msg="Deleted Success")
|
||||
|
||||
|
||||
@router.get("/authorized", summary="查看权限")
|
||||
async def get_role_authorized(id: int = Query(..., description="ID")):
|
||||
role_obj = await datasource_controller.get(id=id)
|
||||
data = await role_obj.to_dict(m2m=True)
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
@router.post("/authorized", summary="更新权限")
|
||||
async def update_role_authorized(role_in: RoleUpdateMenusApis):
|
||||
role_obj = await datasource_controller.get(id=role_in.id)
|
||||
await datasource_controller.update_roles(role=role_obj, menu_ids=role_in.menu_ids, api_infos=role_in.api_infos)
|
||||
return Success(msg="Updated Successfully")
|
||||
@@ -0,0 +1,81 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
from fastapi.exceptions import HTTPException
|
||||
from tortoise.expressions import Q
|
||||
|
||||
from app.controllers.table import codegen_controller
|
||||
from app.schemas.base import Success, SuccessExtra
|
||||
from app.schemas.codegen import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/table")
|
||||
|
||||
|
||||
@router.get("/page", summary="查看列表")
|
||||
async def list_role(
|
||||
page: int = Query(1, description="页码"),
|
||||
limit: int = Query(10, description="每页数量"),
|
||||
table_name: str = Query("", description="名称,用于查询"),
|
||||
):
|
||||
q = Q()
|
||||
if table_name:
|
||||
q = Q(name__contains=table_name)
|
||||
total, role_objs = await codegen_controller.list(page=page, page_size=limit, search=q)
|
||||
data = [await obj.to_dict() for obj in role_objs]
|
||||
return SuccessExtra(data=dict(list=data, total=total, page=page, page_size=limit))
|
||||
|
||||
|
||||
@router.post("/import/{db_connection}", summary="导入表")
|
||||
async def get_role(db_connection, table: dict = None):
|
||||
print('db_connection, importTables', db_connection, table)
|
||||
data = await codegen_controller.import_table(db_connection, importTables=table['table'])
|
||||
# role_obj = await codegen_controller.get(id=role_id)
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
import json
|
||||
@router.put("/field/{id}", summary="更新字段")
|
||||
async def update_fields(id: int, fields_in: dict):
|
||||
fields_in['fields'] = json.dumps(fields_in['fields'])
|
||||
await codegen_controller.update(id=id, obj_in=fields_in)
|
||||
return Success(msg="Updated Successfully")
|
||||
|
||||
@router.get("/preview/{id}", summary="代码预览")
|
||||
async def code_preview(id: int):
|
||||
data = await codegen_controller.preview(id)
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
@router.get("/{id}", summary="查看详细的表信息")
|
||||
async def get_table_detail(id):
|
||||
table_obj = await codegen_controller.get(id=id)
|
||||
return Success(data=await table_obj.to_dict())
|
||||
|
||||
@router.post("/update", summary="更新")
|
||||
async def update_role(role_in):
|
||||
await codegen_controller.update(id=role_in.id, obj_in=role_in)
|
||||
return Success(msg="Updated Successfully")
|
||||
|
||||
|
||||
@router.delete("/", summary="删除")
|
||||
async def delete_role(
|
||||
batch_ids: dict
|
||||
):
|
||||
for id in batch_ids.get('data'):
|
||||
await codegen_controller.remove(id=id)
|
||||
return Success(msg="Deleted Success")
|
||||
|
||||
|
||||
@router.get("/authorized", summary="查看权限")
|
||||
async def get_role_authorized(id: int = Query(..., description="ID")):
|
||||
role_obj = await codegen_controller.get(id=id)
|
||||
data = await role_obj.to_dict(m2m=True)
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
@router.post("/authorized", summary="更新权限")
|
||||
async def update_role_authorized(role_in):
|
||||
role_obj = await codegen_controller.get(id=role_in.id)
|
||||
await codegen_controller.update_roles(role=role_obj, menu_ids=role_in.menu_ids, api_infos=role_in.api_infos)
|
||||
return Success(msg="Updated Successfully")
|
||||
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .depts import router
|
||||
|
||||
depts_router = APIRouter()
|
||||
depts_router.include_router(router, tags=["部门模块"])
|
||||
|
||||
__all__ = ["depts_router"]
|
||||
@@ -0,0 +1,48 @@
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from app.controllers.dept import dept_controller
|
||||
from app.schemas import Success
|
||||
from app.schemas.depts import *
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/list", summary="查看部门列表")
|
||||
async def list_dept(
|
||||
name: str = Query(None, description="部门名称"),
|
||||
):
|
||||
dept_tree = await dept_controller.get_dept_tree(name)
|
||||
return Success(data=dept_tree)
|
||||
|
||||
|
||||
@router.get("/get", summary="查看部门")
|
||||
async def get_dept(
|
||||
id: int = Query(..., description="部门ID"),
|
||||
):
|
||||
dept_obj = await dept_controller.get(id=id)
|
||||
data = await dept_obj.to_dict()
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
@router.post("/create", summary="创建部门")
|
||||
async def create_dept(
|
||||
dept_in: DeptCreate,
|
||||
):
|
||||
await dept_controller.create_dept(obj_in=dept_in)
|
||||
return Success(msg="Created Successfully")
|
||||
|
||||
|
||||
@router.post("/update", summary="更新部门")
|
||||
async def update_dept(
|
||||
dept_in: DeptUpdate,
|
||||
):
|
||||
await dept_controller.update_dept(obj_in=dept_in)
|
||||
return Success(msg="Update Successfully")
|
||||
|
||||
|
||||
@router.delete("/delete", summary="删除部门")
|
||||
async def delete_dept(
|
||||
dept_id: int = Query(..., description="部门ID"),
|
||||
):
|
||||
await dept_controller.delete_dept(dept_id=dept_id)
|
||||
return Success(msg="Deleted Success")
|
||||
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .finance_parse import router
|
||||
|
||||
finance_parse_router = APIRouter()
|
||||
finance_parse_router.include_router(router, tags=["订单备注解析模块"])
|
||||
|
||||
__all__ = ["finance_parse_router"]
|
||||
@@ -0,0 +1,95 @@
|
||||
from fastapi import APIRouter, File, UploadFile, Query
|
||||
from fastapi.responses import FileResponse
|
||||
from app.schemas import Success, SuccessExtra, Fail
|
||||
from app.controllers.finance_parse import task_controller, parse_finance_data
|
||||
import os, time
|
||||
from app.schemas.task import DecodeTaskParams, DecodeTaskResult, TaskResponse
|
||||
from app.models.automation import TaskStatus, TaskType
|
||||
from datetime import datetime
|
||||
router = APIRouter()
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from app.utils.excel_utils import get_sheets_and_headers
|
||||
|
||||
|
||||
@router.post("/upload", summary="上传文件")
|
||||
async def upload_file(file: UploadFile = File(...)):
|
||||
# file.filename: 原始文件名
|
||||
# file.content_type: MIME 类型
|
||||
# file.file: 类文件对象(SpooledTemporaryFile)
|
||||
|
||||
# 保存文件到本地(示例:保存到 ./uploads/)
|
||||
upload_dir = Path("uploads")
|
||||
upload_dir.mkdir(exist_ok=True)
|
||||
filename = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{file.filename}"
|
||||
|
||||
file_path = upload_dir / filename
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
# 获取所有 sheet 名称及其表头
|
||||
sheets_headers = get_sheets_and_headers(file_path)
|
||||
|
||||
# # 打印结果
|
||||
# for sheet_name, headers in sheets_headers.items():
|
||||
# print(f"Sheet: {sheet_name}")
|
||||
# print(f"Headers: {headers}\n")
|
||||
|
||||
data = {
|
||||
"filename": filename,
|
||||
"data": sheets_headers,
|
||||
}
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
@router.post("/parse", summary="解析文件")
|
||||
async def parse_file(data: dict):
|
||||
upload_dir = Path("uploads")
|
||||
filename = upload_dir / data["filename"]
|
||||
sheet = data["sheet"]
|
||||
header = data["header"]
|
||||
parse_type = data["parse_type"]
|
||||
taskname = data["filename"] + "_" + sheet
|
||||
|
||||
print(filename, sheet, header, parse_type)
|
||||
task, task_obj = await task_controller.create_task(name=taskname, obj_in=DecodeTaskParams(filename=str(filename), sheet_name=sheet, header=header, decode_type=parse_type))
|
||||
total_amount = 0
|
||||
start = time.time()
|
||||
|
||||
try:
|
||||
result_file_path, total_amount = parse_finance_data(
|
||||
filename,
|
||||
target_index=header,
|
||||
is_horizontal=(parse_type == "horizontal"),
|
||||
sheet_name=sheet
|
||||
)
|
||||
except Exception as e:
|
||||
return Fail(msg=f"解析失败: {str(e)}", code=400)
|
||||
|
||||
if not os.path.exists(result_file_path):
|
||||
return Fail(msg=f"解析结果文件未生成", code=404)
|
||||
|
||||
# 提取原始文件名(不含路径),用于下载时的默认文件名
|
||||
download_filename = os.path.basename(result_file_path)
|
||||
await task_controller.update_task(task_obj, task.id, TaskStatus.SUCCESS, DecodeTaskResult(
|
||||
filename=str(result_file_path),
|
||||
spend=time.time() - start,
|
||||
rows=total_amount,
|
||||
))
|
||||
|
||||
return FileResponse(
|
||||
path=result_file_path,
|
||||
filename=download_filename, # 浏览器下载时显示的文件名
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' # .xlsx
|
||||
)
|
||||
|
||||
@router.get("/list", summary="获取任务列表")
|
||||
async def get_tasks(page: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
name: str = Query("", description="任务名称,用于查询"),
|
||||
type: TaskType = Query(None, description="任务类型,用于查询")):
|
||||
total, tasks = await task_controller.list(name, type, page, page_size)
|
||||
data = [TaskResponse.from_orm(task).model_dump() for task in tasks]
|
||||
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
|
||||
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .menus import router
|
||||
|
||||
menus_router = APIRouter()
|
||||
menus_router.include_router(router, tags=["菜单模块"])
|
||||
|
||||
__all__ = ["menus_router"]
|
||||
@@ -0,0 +1,63 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from app.controllers.menu import menu_controller
|
||||
from app.schemas.base import Fail, Success, SuccessExtra
|
||||
from app.schemas.menus import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/list", summary="查看菜单列表")
|
||||
async def list_menu(
|
||||
page: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
):
|
||||
async def get_menu_with_children(menu_id: int):
|
||||
menu = await menu_controller.model.get(id=menu_id)
|
||||
menu_dict = await menu.to_dict()
|
||||
child_menus = await menu_controller.model.filter(parent_id=menu_id).order_by("order")
|
||||
menu_dict["children"] = [await get_menu_with_children(child.id) for child in child_menus]
|
||||
return menu_dict
|
||||
|
||||
parent_menus = await menu_controller.model.filter(parent_id=0).order_by("order")
|
||||
res_menu = [await get_menu_with_children(menu.id) for menu in parent_menus]
|
||||
return SuccessExtra(data=res_menu, total=len(res_menu), page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get("/get", summary="查看菜单")
|
||||
async def get_menu(
|
||||
menu_id: int = Query(..., description="菜单id"),
|
||||
):
|
||||
result = await menu_controller.get(id=menu_id)
|
||||
return Success(data=result)
|
||||
|
||||
|
||||
@router.post("/create", summary="创建菜单")
|
||||
async def create_menu(
|
||||
menu_in: MenuCreate,
|
||||
):
|
||||
await menu_controller.create(obj_in=menu_in)
|
||||
return Success(msg="Created Success")
|
||||
|
||||
|
||||
@router.post("/update", summary="更新菜单")
|
||||
async def update_menu(
|
||||
menu_in: MenuUpdate,
|
||||
):
|
||||
await menu_controller.update(id=menu_in.id, obj_in=menu_in)
|
||||
return Success(msg="Updated Success")
|
||||
|
||||
|
||||
@router.delete("/delete", summary="删除菜单")
|
||||
async def delete_menu(
|
||||
id: int = Query(..., description="菜单id"),
|
||||
):
|
||||
child_menu_count = await menu_controller.model.filter(parent_id=id).count()
|
||||
if child_menu_count > 0:
|
||||
return Fail(msg="Cannot delete a menu with child menus")
|
||||
await menu_controller.remove(id=id)
|
||||
return Success(msg="Deleted Success")
|
||||
@@ -0,0 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
from .msg import router as msg_router
|
||||
from .event import router as event_router
|
||||
|
||||
msg_route_router = APIRouter()
|
||||
msg_route_router.include_router(msg_router, tags=["消息推送模块"])
|
||||
msg_route_router.include_router(event_router, tags=["事件模块"])
|
||||
|
||||
__all__ = ["msg_route_router"]
|
||||
@@ -0,0 +1,15 @@
|
||||
# app/api/wechat.py
|
||||
from fastapi import APIRouter
|
||||
from app.schemas.msg import FeishuEvent
|
||||
from app.http_base import unified_resp
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/event")
|
||||
|
||||
# 群聊入口
|
||||
@router.post("/feishu", summary="通过群ID获取到群信息以及同步数据到本地")
|
||||
async def feishu(feishuEvent: FeishuEvent):
|
||||
return {"challenge": feishuEvent.challenge}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# app/api/wechat.py
|
||||
from fastapi import APIRouter, Depends
|
||||
from app.controllers.msg import msg_controller
|
||||
from app.controllers.weixin.customer import weixin_customer_controller
|
||||
from app.schemas.msg import MsgNewOrder, MsgFilter, MsgUpdate
|
||||
from app.http_base import unified_resp
|
||||
from app.schemas.apis import Paginate
|
||||
from tortoise.expressions import Q
|
||||
from ..weixin.base import get_weixin_user
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/msg')
|
||||
|
||||
# 群聊入口
|
||||
@router.post("/new_order", summary="通过群ID获取到群信息以及同步数据到本地")
|
||||
@unified_resp
|
||||
async def create_new_order(msg: MsgNewOrder):
|
||||
|
||||
if msg.buyer_id.startswith('*******'):
|
||||
return {"msg": "不支持的客户类型"}
|
||||
|
||||
if msg.buyer_nick:
|
||||
weixin_customer = await weixin_customer_controller.model.filter(taobao_name=msg.buyer_nick).first()
|
||||
elif msg.buyer_id:
|
||||
weixin_customer = await weixin_customer_controller.model.filter(weixin_id=msg.buyer_id).first()
|
||||
else:
|
||||
return {"msg": "buyer_id or buyer_nick is required"}
|
||||
|
||||
if not weixin_customer:
|
||||
return {"msg": "当前客户未绑定微信账号"}
|
||||
|
||||
logger.info(f'用户新订单: {msg}')
|
||||
|
||||
order_id = msg.order_id
|
||||
|
||||
await msg_controller.new_order(order_id, weixin_customer, is_refund=msg.is_refund)
|
||||
await msg_controller.sync_msg()
|
||||
return {"msg": "success"}
|
||||
|
||||
|
||||
@router.get("/fill_customer_info", summary="填充客户信息")
|
||||
@unified_resp
|
||||
async def fill_customer_info():
|
||||
await msg_controller.fill_customer_info()
|
||||
return {"msg": "success"}
|
||||
|
||||
@router.get("/list", summary="通过群ID获取到群信息以及同步数据到本地")
|
||||
@unified_resp
|
||||
async def list_msg(paginate: Paginate = Depends(), order: str = "-id", msg_filter: MsgFilter = Depends(), weixin_user: dict = Depends(get_weixin_user)):
|
||||
print('paginate', paginate.page, paginate.page_size)
|
||||
print('msg_filter', msg_filter)
|
||||
print('weixin_user', weixin_user.username, weixin_user.userid)
|
||||
# q = Q(owner_id='11')
|
||||
q = Q(owner_id=weixin_user.userid)
|
||||
if msg_filter.type is not None:
|
||||
q &= Q(type=msg_filter.type)
|
||||
if msg_filter.is_read is not None:
|
||||
q &= Q(is_read=msg_filter.is_read)
|
||||
if msg_filter.is_read:
|
||||
order = '-read_at'
|
||||
|
||||
if msg_filter.is_refund is not None:
|
||||
q &= Q(detail__contains={"is_refund": 1 if msg_filter.is_refund else 0})
|
||||
|
||||
total, msg_list = await msg_controller.list(paginate.page, paginate.page_size, order=[order], search=q)
|
||||
return {"count": total, "lists": [ await msg.to_dict() for msg in msg_list]}
|
||||
|
||||
@router.post("/set_read", summary="设置消息为已读")
|
||||
@unified_resp
|
||||
async def set_read(msg: MsgUpdate, weixin_user: dict = Depends(get_weixin_user)):
|
||||
await msg_controller.set_read(msg.id)
|
||||
return {"msg": "success"}
|
||||
|
||||
@router.get("/get_order_user_days_before", summary="获取指定天数前的订单用户")
|
||||
@unified_resp
|
||||
async def get_order_user_days_before():
|
||||
return await msg_controller.get_order_user_days_before(2)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .roles import router
|
||||
|
||||
roles_router = APIRouter()
|
||||
roles_router.include_router(router, tags=["角色模块"])
|
||||
|
||||
__all__ = ["roles_router"]
|
||||
@@ -0,0 +1,73 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
from fastapi.exceptions import HTTPException
|
||||
from tortoise.expressions import Q
|
||||
|
||||
from app.controllers import role_controller
|
||||
from app.schemas.base import Success, SuccessExtra
|
||||
from app.schemas.roles import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/list", summary="查看角色列表")
|
||||
async def list_role(
|
||||
page: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
role_name: str = Query("", description="角色名称,用于查询"),
|
||||
):
|
||||
q = Q()
|
||||
if role_name:
|
||||
q = Q(name__contains=role_name)
|
||||
total, role_objs = await role_controller.list(page=page, page_size=page_size, search=q)
|
||||
data = [await obj.to_dict() for obj in role_objs]
|
||||
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get("/get", summary="查看角色")
|
||||
async def get_role(
|
||||
role_id: int = Query(..., description="角色ID"),
|
||||
):
|
||||
role_obj = await role_controller.get(id=role_id)
|
||||
return Success(data=await role_obj.to_dict())
|
||||
|
||||
|
||||
@router.post("/create", summary="创建角色")
|
||||
async def create_role(role_in: RoleCreate):
|
||||
if await role_controller.is_exist(name=role_in.name):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="The role with this rolename already exists in the system.",
|
||||
)
|
||||
await role_controller.create(obj_in=role_in)
|
||||
return Success(msg="Created Successfully")
|
||||
|
||||
|
||||
@router.post("/update", summary="更新角色")
|
||||
async def update_role(role_in: RoleUpdate):
|
||||
await role_controller.update(id=role_in.id, obj_in=role_in)
|
||||
return Success(msg="Updated Successfully")
|
||||
|
||||
|
||||
@router.delete("/delete", summary="删除角色")
|
||||
async def delete_role(
|
||||
role_id: int = Query(..., description="角色ID"),
|
||||
):
|
||||
await role_controller.remove(id=role_id)
|
||||
return Success(msg="Deleted Success")
|
||||
|
||||
|
||||
@router.get("/authorized", summary="查看角色权限")
|
||||
async def get_role_authorized(id: int = Query(..., description="角色ID")):
|
||||
role_obj = await role_controller.get(id=id)
|
||||
data = await role_obj.to_dict(m2m=True)
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
@router.post("/authorized", summary="更新角色权限")
|
||||
async def update_role_authorized(role_in: RoleUpdateMenusApis):
|
||||
role_obj = await role_controller.get(id=role_in.id)
|
||||
await role_controller.update_roles(role=role_obj, menu_ids=role_in.menu_ids, api_infos=role_in.api_infos)
|
||||
return Success(msg="Updated Successfully")
|
||||
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .users import router
|
||||
|
||||
users_router = APIRouter()
|
||||
users_router.include_router(router, tags=["用户模块"])
|
||||
|
||||
__all__ = ["users_router"]
|
||||
@@ -0,0 +1,87 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Body, Query
|
||||
from tortoise.expressions import Q
|
||||
|
||||
from app.controllers.dept import dept_controller
|
||||
from app.controllers.user import user_controller
|
||||
from app.schemas.base import Fail, Success, SuccessExtra
|
||||
from app.schemas.users import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/list", summary="查看用户列表")
|
||||
async def list_user(
|
||||
page: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
username: str = Query("", description="用户名称,用于搜索"),
|
||||
email: str = Query("", description="邮箱地址"),
|
||||
dept_id: int = Query(None, description="部门ID"),
|
||||
):
|
||||
q = Q()
|
||||
if username:
|
||||
q &= Q(username__contains=username)
|
||||
if email:
|
||||
q &= Q(email__contains=email)
|
||||
if dept_id is not None:
|
||||
q &= Q(dept_id=dept_id)
|
||||
total, user_objs = await user_controller.list(page=page, page_size=page_size, search=q)
|
||||
data = [await obj.to_dict(m2m=True, exclude_fields=["password"]) for obj in user_objs]
|
||||
for item in data:
|
||||
dept_id = item.pop("dept_id", None)
|
||||
item["dept"] = await (await dept_controller.get(id=dept_id)).to_dict() if dept_id else {}
|
||||
|
||||
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get("/get", summary="查看用户")
|
||||
async def get_user(
|
||||
user_id: int = Query(..., description="用户ID"),
|
||||
):
|
||||
user_obj = await user_controller.get(id=user_id)
|
||||
user_dict = await user_obj.to_dict(exclude_fields=["password"])
|
||||
return Success(data=user_dict)
|
||||
|
||||
|
||||
@router.post("/create", summary="创建用户")
|
||||
async def create_user(
|
||||
user_in: UserCreate,
|
||||
):
|
||||
user = await user_controller.get_by_email(user_in.email)
|
||||
if user:
|
||||
return Fail(code=400, msg="The user with this email already exists in the system.")
|
||||
new_user = await user_controller.create_user(obj_in=user_in)
|
||||
await user_controller.update_roles(new_user, user_in.role_ids)
|
||||
return Success(msg="Created Successfully")
|
||||
|
||||
|
||||
@router.post("/update", summary="用户管理中更新用户")
|
||||
async def update_user(
|
||||
user_in: UserUpdate,
|
||||
):
|
||||
user = await user_controller.update(id=user_in.id, obj_in=user_in)
|
||||
await user_controller.update_roles(user, user_in.role_ids)
|
||||
return Success(msg="Updated Successfully")
|
||||
|
||||
@router.post("/update_user_online", summary="更新用户")
|
||||
async def update_user_online(
|
||||
user_in: UserUpdateOnline,
|
||||
):
|
||||
user = await user_controller.update(id=user_in.id, obj_in=user_in)
|
||||
return Success(msg="Updated Successfully")
|
||||
|
||||
@router.delete("/delete", summary="删除用户")
|
||||
async def delete_user(
|
||||
user_id: int = Query(..., description="用户ID"),
|
||||
):
|
||||
await user_controller.remove(id=user_id)
|
||||
return Success(msg="Deleted Successfully")
|
||||
|
||||
|
||||
@router.post("/reset_password", summary="重置密码")
|
||||
async def reset_password(user_id: int = Body(..., description="用户ID", embed=True)):
|
||||
await user_controller.reset_password(user_id)
|
||||
return Success(msg="密码已重置为123456")
|
||||
@@ -0,0 +1,11 @@
|
||||
from fastapi import APIRouter
|
||||
from app.controllers.weixin import schedule as _schedule
|
||||
from .weixin import router
|
||||
from .weixin_user import router as weixin_user_router
|
||||
|
||||
|
||||
weixin_router = APIRouter()
|
||||
weixin_router.include_router(router, tags=["微信模块"])
|
||||
weixin_router.include_router(weixin_user_router, tags=["微信用户模块"])
|
||||
|
||||
__all__ = ["weixin_router"]
|
||||
@@ -0,0 +1,16 @@
|
||||
# app/core/dependency.py
|
||||
from typing import Annotated
|
||||
from fastapi import Header, HTTPException, Depends
|
||||
from app.core.ctx import set_ctx_weixin_user
|
||||
from app.models import WeixinUser
|
||||
|
||||
async def get_weixin_user(token: str = Header(..., description="token验证")) -> WeixinUser:
|
||||
user = await WeixinUser.filter(userid=token).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="Weixin User Authentication failed")
|
||||
# CTX_USER_ID.set(int(user.id))
|
||||
set_ctx_weixin_user(int(user.id), f'{user.username}({user.english_name})')
|
||||
return user
|
||||
|
||||
DependWeixinUser = Depends(get_weixin_user)
|
||||
WeixinUserDep = Annotated[WeixinUser, DependWeixinUser]
|
||||
@@ -0,0 +1,2 @@
|
||||
httpx
|
||||
pypinyin
|
||||
@@ -0,0 +1,95 @@
|
||||
# app/api/wechat.py
|
||||
from fastapi import APIRouter, Query
|
||||
# from app.controllers import wechat_sdk
|
||||
# from app.controllers.weixin_script import WeixinUser_controller
|
||||
from some_sdk.services.binder import xy_client
|
||||
from some_sdk.wk_weixin_sdk import auth
|
||||
from some_sdk.wk_weixin_sdk.apis import corp_group, extern_user
|
||||
from app.http_base import unified_resp
|
||||
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# @router.get("/app-config")
|
||||
# @unified_resp
|
||||
# def get_config():
|
||||
# return wechat_sdk.get_base_config()
|
||||
|
||||
# 获取企业身份签名
|
||||
@router.get("/jssdk-config", summary="获取企业微信 企业身份 JS-SDK 配置")
|
||||
@unified_resp
|
||||
async def jssdk_config(type: str = Query(..., description="类型"), url: str = Query(..., description="前端当前页面 URL(不含 #)")):
|
||||
return await auth.get_jssdk_config(type, url)
|
||||
|
||||
@router.get("/get_access_user_info", summary="获取访问用户身份")
|
||||
@unified_resp
|
||||
async def get_access_user_info(code: str = Query(..., description="code为企业成员点击了构造链接之后附加在url中参数")):
|
||||
return await auth.get_access_user_info(code)
|
||||
|
||||
@router.get("/get_external_group_chat_info", summary="获取外部群聊详细信息")
|
||||
@unified_resp
|
||||
async def get_external_group_chat_info(chat_id: str = Query(..., description="群对话ID")):
|
||||
return await corp_group.get_external_group_chat_info(xy_client, chat_id)
|
||||
|
||||
@router.get("/get_external_user_chat_info", summary="获取外部客户详细信息")
|
||||
@unified_resp
|
||||
async def get_external_user_chat_info(user_id: str = Query(..., description="为外部客户的userid")):
|
||||
return await extern_user.get_external_user_chat_info(user_id)
|
||||
|
||||
# @router.get("/get_external_user_list", summary="获取指定用户的所有外部客户")
|
||||
# @unified_resp
|
||||
# async def get_external_user_list(user_id: str = Query(..., description="企业成员的userid")):
|
||||
# return await wechat_sdk.get_external_user_list(user_id)
|
||||
|
||||
# @router.get("/get_corp_user_id_list", summary="获取指公司所有员工")
|
||||
# @unified_resp
|
||||
# async def get_corp_user_id_list():
|
||||
# return await wechat_sdk.get_corp_user_id_list()
|
||||
|
||||
# @router.get("/get_follow_user_list", summary="获取配置了客户联系功能的成员列表")
|
||||
# @unified_resp
|
||||
# async def get_follow_user_list():
|
||||
# return await wechat_sdk.get_follow_user_list()
|
||||
|
||||
# @router.get("/get_follow_user_list", summary="获取配置了客户联系功能的成员列表")
|
||||
# @unified_resp
|
||||
# async def get_follow_user_list():
|
||||
# return await wechat_sdk.get_follow_user_list()
|
||||
|
||||
# @router.get("/get_groupchat_list", summary="获取指定用户的客户群列表")
|
||||
# @unified_resp
|
||||
# async def get_groupchat_list():
|
||||
# return await wechat_sdk.get_groupchat_list(['XiYinShuo'])
|
||||
|
||||
# @router.get("/update_all_user", summary="同步所有用户信息")
|
||||
# @unified_resp
|
||||
# async def update_all_user():
|
||||
# return await WeixinUser_controller.update_all_user()
|
||||
|
||||
# @router.get("/update_all_user_by_group_chat", summary="通过群聊同步用户信息")
|
||||
# @unified_resp
|
||||
# async def update_all_user_by_group_chat():
|
||||
# return await WeixinUser_controller.update_all_user_by_group_chat()
|
||||
|
||||
# @router.get("/convert_extenal_userid", summary="通过群聊同步用户信息")
|
||||
# @unified_resp
|
||||
# async def convert_extenal_userid():
|
||||
# return await wechat_sdk.convert_extenal_userid('wmKgOaDQAA37oxGfhBFCAFPqmICXNAZA')
|
||||
|
||||
# @router.get("/update_all_user_by_xingyun", summary="通过外部系统同步用户信息")
|
||||
# @unified_resp
|
||||
# async def update_all_user_by_xingyun():
|
||||
# return await WeixinUser_controller.update_all_user_by_xingyun()
|
||||
|
||||
# @router.get("/convert_extenal_userid", summary="转换三方应用的extenal_userid")
|
||||
# @unified_resp
|
||||
# async def convert_extenal_userid(user_id: str = Query(..., description="客户的userid")):
|
||||
# return await wechat_sdk.convert_extenal_userid(user_id)
|
||||
|
||||
# @router.get("/get_external_contact_list", summary="获取已服务的外部联系人")
|
||||
# @unified_resp
|
||||
# async def get_external_contact_list(cursor: Optional[str] = Query(None, description="客户的userid")):
|
||||
# return await wechat_sdk.get_external_contact_list(cursor)
|
||||
@@ -0,0 +1,294 @@
|
||||
# app/api/wechat.py
|
||||
from fastapi import APIRouter, Query, BackgroundTasks, Depends
|
||||
from tortoise.expressions import Q
|
||||
from app.controllers.weixin.utils import get_buyer_nick_from_group_name
|
||||
from app.controllers.weixin.group import weixin_group_chat_controller
|
||||
from app.controllers.weixin.customer import weixin_customer_controller
|
||||
from app.controllers.weixin.user import weixin_user_controller
|
||||
from app.controllers.action import action_controller
|
||||
from app.schemas.weixin import WeixinUserBindInfo, WeixinOrderBindInfo, TriggerWeixinGroupChat, WeixinOrderRemark, WeixinCustomerFilter, WeixinCustomerCreate
|
||||
from app.http_base import unified_resp
|
||||
from app.schemas.apis import Paginate
|
||||
from .base import get_weixin_user
|
||||
|
||||
from .base import WeixinUserDep
|
||||
from app.models.msg import ActionType
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from app.utils.event_task import event_manager, EventType
|
||||
from app.core.cache import cache_if
|
||||
# from app.schemas.crm import CrmBindInfoCreate, CrmBindInfoUpdate
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="")
|
||||
|
||||
# 废弃
|
||||
@router.post("/list_user_join_group", summary="加载用户加入的群聊列表")
|
||||
@unified_resp
|
||||
async def list_user_join_group(customer: WeixinCustomerCreate, weixin_user: dict = Depends(get_weixin_user)):
|
||||
print(f'list_user_join_group, customer: {customer}')
|
||||
# user_join_group = await weixin_group_chat_controller.list_user_join_group(customer.xingyun_id)
|
||||
user_join_group = await weixin_customer_controller.get_user_detail(customer.weixin_id)
|
||||
return user_join_group
|
||||
|
||||
@router.get("/list_customer", summary="加载客户列表")
|
||||
@unified_resp
|
||||
async def list_msg(paginate: Paginate = Depends(), order: str = "-id", customer_filter: WeixinCustomerFilter = Depends(), weixin_user: dict = Depends(get_weixin_user)):
|
||||
print('paginate', paginate.page, paginate.page_size)
|
||||
print('customer_filter', customer_filter)
|
||||
print('weixin_user', weixin_user.username, weixin_user.userid)
|
||||
|
||||
q = Q()
|
||||
data = {}
|
||||
if customer_filter.order_id is not None:
|
||||
q &= Q(order_id=customer_filter.order_id)
|
||||
data['order_id'] = customer_filter.order_id
|
||||
if customer_filter.shop_name is not None:
|
||||
q &= Q(shop_name__contains=customer_filter.shop_name)
|
||||
data['shop_name'] = customer_filter.shop_name
|
||||
if customer_filter.union_id is not None:
|
||||
# 是或者的关系 weixin_id\xingyun_id\taobao_id\union_id
|
||||
q &= (Q(weixin_id=customer_filter.union_id) | Q(xingyun_id=customer_filter.union_id) | Q(taobao_id=customer_filter.union_id) | Q(union_id=customer_filter.union_id))
|
||||
data['union_id'] = customer_filter.union_id
|
||||
if customer_filter.union_name is not None:
|
||||
# 是或者的关系 weixin_username\xingyun_name\taobao_name
|
||||
q &= (Q(weixin_name__contains=customer_filter.union_name) | Q(xingyun_name__contains=customer_filter.union_name) | Q(taobao_name__contains=customer_filter.union_name))
|
||||
data['union_name'] = customer_filter.union_name
|
||||
if customer_filter.xingyun_tags is not None:
|
||||
q &= Q(xingyun_tags__contains={"tagName": customer_filter.xingyun_tags})
|
||||
data['xingyun_tags'] = customer_filter.xingyun_tags
|
||||
if customer_filter.has_order is not None:
|
||||
q &= Q(order_id__isnull=not customer_filter.has_order)
|
||||
data['has_order'] = customer_filter.has_order
|
||||
|
||||
# 打印查询条件明文
|
||||
print('raw query', data)
|
||||
|
||||
total, user_list = await weixin_customer_controller.list(paginate.page, paginate.page_size, order=[order], search=q)
|
||||
|
||||
lists = []
|
||||
for user in user_list:
|
||||
user_info = user.dump_dict()
|
||||
lists.append(user_info)
|
||||
user_info['user_join_group'] = await weixin_customer_controller.get_user_detail(user.weixin_id)
|
||||
|
||||
return {"count": total, "lists": lists}
|
||||
|
||||
|
||||
# 群聊入口
|
||||
@router.get("/get_user_info", summary="通过群ID获取到群信息以及同步数据到本地")
|
||||
@unified_resp
|
||||
async def get_external_group_chat_info(userid: str):
|
||||
weixin_user = await weixin_user_controller.model.filter(userid=userid).first()
|
||||
return weixin_user.to_dict()
|
||||
|
||||
# 群聊入口
|
||||
@router.get("/group/get_external_group_chat_info", summary="通过群ID获取到群信息以及同步数据到本地")
|
||||
@unified_resp
|
||||
async def get_external_group_chat_info(chat_id: str, background_tasks: BackgroundTasks):
|
||||
group_info = await weixin_group_chat_controller.get_external_group_chat_info(chat_id)
|
||||
|
||||
if group_info['detect_update']:
|
||||
await event_manager.publish_async(EventType.GROUP_CHAT_UPDATED, group_info, background_tasks)
|
||||
|
||||
return group_info
|
||||
|
||||
|
||||
@router.get("/group/get_order_relative_user_list_by_weixin_groupid", summary="通过群ID获取订单相关用户列表")
|
||||
@unified_resp
|
||||
async def get_order_relative_user_list_by_weixin_groupid(chat_id: str = Query(..., description="群ID"), background_tasks: BackgroundTasks = None):
|
||||
async with cache_if(f'order:chat:{chat_id}', ttl=60*3) as cache:
|
||||
if cache.hit:
|
||||
order_list = cache.value
|
||||
if order_list:
|
||||
order_list[0] = await weixin_group_chat_controller.get_user_detail_by_order(order_id=order_list[0]['ctid'])
|
||||
else:
|
||||
order_list = await _get_order_relative_user_list_by_weixin_groupid(chat_id, background_tasks)
|
||||
cache.set(order_list)
|
||||
return order_list
|
||||
|
||||
async def _get_order_relative_user_list_by_weixin_groupid(chat_id: str = Query(..., description="群ID"), background_tasks: BackgroundTasks = None):
|
||||
group_info = await weixin_group_chat_controller.get_external_group_chat_info(chat_id)
|
||||
logger.info(f'group_info: {group_info}')
|
||||
|
||||
if group_info['detect_update'] or True:
|
||||
await event_manager.publish_async(EventType.GROUP_CHAT_UPDATED, group_info, background_tasks)
|
||||
|
||||
name = group_info.get('name')
|
||||
logger.debug(f'群聊名称: {name} chat_id: {chat_id}')
|
||||
|
||||
user_order_list = []
|
||||
|
||||
external_member_list = group_info['external_member_list']
|
||||
if external_member_list:
|
||||
userid_list = [member['userid'] for member in external_member_list if member.get('userid')]
|
||||
logger.debug(f'userid_list: {userid_list}')
|
||||
customer_list = await weixin_customer_controller.model.filter(weixin_id__in=userid_list, taobao_id__isnull=False).all()
|
||||
for customer in customer_list:
|
||||
buyer = customer.taobao_name or customer.taobao_id
|
||||
if not buyer: continue
|
||||
|
||||
logger.debug(f'buyer: {buyer}')
|
||||
result, order_list = await weixin_group_chat_controller.get_order_relative_user_list_by_weixin_userid(customer.weixin_id)
|
||||
if not result: continue
|
||||
user_order_list = order_list
|
||||
break
|
||||
# return await weixin_group_chat_controller.get_order_relative_user_list_by_weixin_group_name(buyer_nick)
|
||||
# return await weixin_group_chat_controller.get_order_relative_user_list_by_weixin_group_buyer_id(buyer_id)
|
||||
|
||||
if not user_order_list:
|
||||
logger.warning(f'群内成员没有订单,将根据群聊名称 {name} 来获取订单相关用户列表')
|
||||
assert name, "当前群聊未命名"
|
||||
buyer_nick = get_buyer_nick_from_group_name(name)
|
||||
logger.debug(f'name: {name}; buyer_nick: {buyer_nick}')
|
||||
assert buyer_nick, "当前群聊未命名"
|
||||
user_order_list = await weixin_group_chat_controller.get_order_relative_user_list_by_weixin_group_name(buyer_nick)
|
||||
|
||||
if group_info['detect_update']:
|
||||
try:
|
||||
await action_controller.check_create_group_action_is_done(group_info, user_order_list[0]['shop_name'] if user_order_list else None)
|
||||
except Exception as e:
|
||||
logger.exception(f'检查创建群聊动作是否完成失败: {e}')
|
||||
|
||||
return user_order_list
|
||||
|
||||
|
||||
|
||||
# @router.get("/group/load_xingyun_group_info", summary="通过群ID获取订单相关用户列表")
|
||||
# @unified_resp
|
||||
# async def load_xingyun_group_info():
|
||||
# return await weixin_group_chat_controller.load_xingyun_group_info()
|
||||
|
||||
|
||||
@router.get("/get_user_detail_by_order", summary="通过订单ID获取订单相关用户详情")
|
||||
@unified_resp
|
||||
async def get_user_detail_by_order(order_id: str):
|
||||
return await weixin_group_chat_controller.get_user_detail_by_order(order_id)
|
||||
|
||||
|
||||
@router.get("/get_erp_order_log", summary="通过订单ID获取订单日志")
|
||||
@unified_resp
|
||||
async def get_erp_order_log(order_id: str, login_user: WeixinUserDep):
|
||||
result = await weixin_group_chat_controller.get_erp_order_log(order_id)
|
||||
try:
|
||||
await action_controller.new_action(ActionType.VIEW_ERP_LOG, login_user, order_id=order_id)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
logger.error(f'员工操作日志记录失败')
|
||||
return result
|
||||
|
||||
|
||||
# 通过私聊入口
|
||||
@router.get("/get_order_relative_user_list_by_weixin_userid", summary="通过用户ID获取订单相关用户列表")
|
||||
@unified_resp
|
||||
async def get_order_relative_user_list_by_weixin_userid(userid: str, background_tasks: BackgroundTasks):
|
||||
result, order_list = await weixin_group_chat_controller.get_order_relative_user_list_by_weixin_userid(userid)
|
||||
if not result:
|
||||
await event_manager.publish_async(EventType.OPEN_PERSONAL_CHAT, {"userid": userid}, background_tasks)
|
||||
|
||||
return order_list
|
||||
|
||||
|
||||
@router.post("/bind_order", summary="绑定订单")
|
||||
@unified_resp
|
||||
async def bind_order(bind_in: WeixinOrderBindInfo, background_tasks: BackgroundTasks, login_user: WeixinUserDep):
|
||||
bind_in.userid = bind_in.userid.strip()
|
||||
bind_in.order_id = bind_in.order_id.strip()
|
||||
logger.info(f'【绑定订单】userid: {bind_in.userid}; order_id: {bind_in.order_id}')
|
||||
|
||||
old_info = await weixin_customer_controller.model.filter(weixin_id=bind_in.userid, order_id=bind_in.order_id).first()
|
||||
assert not old_info, "不用为客户绑定相同订单"
|
||||
|
||||
result, orders = await weixin_customer_controller.bind_order(bind_in)
|
||||
try:
|
||||
await action_controller.new_action(ActionType.BIND_ORDER, login_user, **bind_in.model_dump(exclude_unset=True))
|
||||
if orders and bind_in.remark:
|
||||
orders = [{
|
||||
"title": order.get("title"),
|
||||
"ctid": order.get("ctid"),
|
||||
"trade_no": order.get("trade_no"),
|
||||
"remark": order.get("remark", ""),
|
||||
} for order in orders]
|
||||
|
||||
need_monitor_order_list = await weixin_customer_controller.remark_order_by_order_id(remark=bind_in.remark, orders=orders)
|
||||
await action_controller.new_action(ActionType.WRITE_REMARK, login_user, order_id=bind_in.order_id, remark=bind_in.remark, orders=need_monitor_order_list, done=len(need_monitor_order_list) == 0)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
logger.error(f'员工操作日志记录失败')
|
||||
|
||||
# 用户触发绑定时,及时同步到星云有客中
|
||||
await event_manager.publish_async(EventType.BIND_ORDER_FOR_USER, orders[0] if orders else {}, background_tasks)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/remark_order", summary="为订单添加额外备注")
|
||||
@unified_resp
|
||||
async def remark_order(remark_in: WeixinOrderRemark, login_user: WeixinUserDep):
|
||||
if not remark_in.remark:
|
||||
logger.warning(f'订单备注为空,将不执行任何操作')
|
||||
return False
|
||||
|
||||
try:
|
||||
need_monitor_order_list = await weixin_customer_controller.remark_order_by_order_id(remark=remark_in.remark, order_id=remark_in.order_id)
|
||||
await action_controller.new_action(ActionType.WRITE_REMARK, login_user, order_id=remark_in.order_id, remark=remark_in.remark, orders=need_monitor_order_list, done=len(need_monitor_order_list) == 0)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
logger.error(f'员工操作日志记录失败')
|
||||
return bool(need_monitor_order_list)
|
||||
|
||||
|
||||
@router.post("/bind_user", summary="用户绑定客户")
|
||||
@unified_resp
|
||||
async def bind_user(bind_info: WeixinUserBindInfo, login_user: WeixinUserDep):
|
||||
result = await weixin_customer_controller.bind_user(bind_info=bind_info)
|
||||
try:
|
||||
await action_controller.new_action(ActionType.BIND_USER, login_user, **bind_info.model_dump(exclude_unset=True))
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
logger.error(f'员工操作日志记录失败')
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/bind_inner_user", summary="用户绑定内部公司成员(erp和企微架构不同)")
|
||||
@unified_resp
|
||||
async def bind_inner_user(bind_info: WeixinUserBindInfo, login_user: WeixinUserDep):
|
||||
result = await weixin_user_controller.bind_user(bind_info)
|
||||
try:
|
||||
await action_controller.new_action(ActionType.BIND_USER, login_user, **bind_info.model_dump(exclude_unset=True))
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
logger.error(f'员工操作日志记录失败')
|
||||
return result
|
||||
|
||||
@router.get("/load_recent_user_from_xingyun", summary="加载最近N天的客户数据")
|
||||
@unified_resp
|
||||
async def load_user_from_xingyun(days_range: int = Query(3, description="加载最近N天的客户数据")):
|
||||
|
||||
now_time = datetime.now()
|
||||
start_time = (now_time - timedelta(days=days_range)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_time = now_time + timedelta(days=1)
|
||||
return await weixin_customer_controller.load_user_from_xingyun(add_time_start=start_time, add_time_end=end_time)
|
||||
|
||||
@router.get("/load_all_user_from_xingyun", summary="加载所有客户数据")
|
||||
@unified_resp
|
||||
async def load_all_user_from_xingyun(background_tasks: BackgroundTasks):
|
||||
await event_manager.publish_async(EventType.SYNC_XINGYUN_CONTACT_INFO, {}, background_tasks)
|
||||
return True
|
||||
|
||||
@router.post("/trigger_create_group_chat", summary="触发创建群聊")
|
||||
@unified_resp
|
||||
async def trigger_create_group_chat(group_chat: TriggerWeixinGroupChat, login_user: WeixinUserDep):
|
||||
try:
|
||||
await action_controller.new_action(ActionType.CREATE_GROUP, login_user, **group_chat.model_dump(exclude_unset=True))
|
||||
if group_chat.order_id:
|
||||
username = login_user.english_name or login_user.nickname
|
||||
username = username.split('-印刷')[0]
|
||||
remark = f'企微联系{username}拉群'
|
||||
need_monitor_order_list = await weixin_customer_controller.remark_order_by_order_id(remark=remark, order_id=group_chat.order_id)
|
||||
await action_controller.new_action(ActionType.WRITE_REMARK, login_user, order_id=group_chat.order_id, remark=remark, orders=need_monitor_order_list, done=len(need_monitor_order_list) == 0)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
logger.error(f'员工操作日志记录失败')
|
||||
return True
|
||||
@@ -0,0 +1,2 @@
|
||||
from .role import role_controller as role_controller
|
||||
from .user import user_controller as user_controller
|
||||
@@ -0,0 +1,109 @@
|
||||
from app.core.crud import CRUDBase
|
||||
from app.schemas.msg import (
|
||||
ActionCreate,
|
||||
ActionUpdate,
|
||||
)
|
||||
from app.models.automation import Action, ActionType
|
||||
from app.models.weixin import WeixinCustomer, CustomerGroup, WeixinGroupChat, WeixinUser
|
||||
from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user
|
||||
from some_sdk.services.binder import lintao_client
|
||||
from app.controllers.weixin.customer import weixin_customer_controller
|
||||
from datetime import datetime
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ActionController(CRUDBase[Action, ActionCreate, ActionUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Action)
|
||||
|
||||
async def new_action(self, type: ActionType, user, done=False, **kwargs):
|
||||
action = ActionCreate(userid=user.userid, username=f'{user.username}({user.english_name})', done=done, type=type, detail=kwargs)
|
||||
return await self.create(action)
|
||||
|
||||
async def check_create_group_action_is_done(self, group_info, shop_name: str = None):
|
||||
name = group_info.get('name')
|
||||
chat_id = group_info.get('chat_id')
|
||||
|
||||
# 完成建群操作
|
||||
create_group_action = await self.model.filter(type=ActionType.CREATE_GROUP, done=False, detail__contains={"group_name": name}).order_by('-id').first()
|
||||
if not create_group_action: return
|
||||
|
||||
create_group_action_detail = create_group_action.detail
|
||||
create_group_action_detail.update({
|
||||
"chat_id": chat_id,
|
||||
})
|
||||
|
||||
create_group_action.done = True
|
||||
create_group_action.done_at = datetime.now()
|
||||
create_group_action.result = group_info
|
||||
create_group_action.detail = create_group_action_detail
|
||||
await create_group_action.save()
|
||||
|
||||
# 完成建群操作后,将群成员添加到客户群中
|
||||
weixin_group = await WeixinGroupChat.filter(chat_id=chat_id).first()
|
||||
if not weixin_group: return
|
||||
|
||||
staff = await WeixinUser.filter(userid=create_group_action.userid).first()
|
||||
|
||||
external_user_ids = create_group_action.detail.get('external_user_ids') or []
|
||||
for userid in external_user_ids:
|
||||
customer = await WeixinCustomer.filter(weixin_id=userid).first()
|
||||
if not customer: continue
|
||||
|
||||
membership = await CustomerGroup.create(
|
||||
staff_userid=create_group_action.userid,
|
||||
customer_userid=customer.weixin_id,
|
||||
group_chatid=chat_id,
|
||||
staff=staff,
|
||||
customer=customer,
|
||||
group=weixin_group,
|
||||
shop_name=shop_name,
|
||||
order_id=create_group_action.detail.get('order_id'),
|
||||
join_time=create_group_action.created_at,
|
||||
)
|
||||
logger.info(f'【建群成功】保存客户的群聊 {name}({chat_id}) 信息')
|
||||
|
||||
async def check_remark_action_is_done(self):
|
||||
actions = await self.model.filter(done=False, type=ActionType.WRITE_REMARK)
|
||||
logger.info(f'检查备注操作是否完成,监控列表中:共有{len(actions)}条')
|
||||
|
||||
update_list = []
|
||||
for action in actions:
|
||||
action_info = action.detail
|
||||
order_id = action_info.get("order_id")
|
||||
remark = action_info.get("remark")
|
||||
if not order_id: continue
|
||||
|
||||
done = False
|
||||
action_result = []
|
||||
|
||||
if remark:
|
||||
async for order in get_order_relative_user(lintao_client, trade_no=order_id):
|
||||
|
||||
try:
|
||||
await weixin_customer_controller.remark_order(order, remark=remark)
|
||||
if order.get('title'):
|
||||
logger.info(f'【监控列表中,订单被领单】重新为订单 {order.get("trade_no")} 打上备注 {remark}')
|
||||
# 未领单的订单,需要添加到监控列表中进行监控,防止被刷掉
|
||||
done = True
|
||||
action_result.append({
|
||||
"trade_no": order.get("trade_no"),
|
||||
"title": order.get("title"),
|
||||
"remark": remark,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception(f'【监控列表中,订单被领单】重新为订单为订单 {order.get("trade_no")} 打上备注 {remark} 失败,异常: {e}')
|
||||
|
||||
if done or not remark:
|
||||
action.done = True
|
||||
action.done_at = datetime.now()
|
||||
action.result = action_result
|
||||
update_list.append(action)
|
||||
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, fields=['done', 'result', 'done_at'])
|
||||
|
||||
|
||||
action_controller = ActionController()
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
from fastapi.routing import APIRoute
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
import logging
|
||||
from app.models.admin import Api
|
||||
from app.schemas.apis import ApiCreate, ApiUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ApiController(CRUDBase[Api, ApiCreate, ApiUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Api)
|
||||
|
||||
async def refresh_api(self):
|
||||
from app import app
|
||||
|
||||
# 删除废弃API数据
|
||||
all_api_list = []
|
||||
for route in app.routes:
|
||||
# 只更新有鉴权的API
|
||||
if isinstance(route, APIRoute) and len(route.dependencies) > 0:
|
||||
all_api_list.append((list(route.methods)[0], route.path_format))
|
||||
delete_api = []
|
||||
for api in await Api.all():
|
||||
if (api.method, api.path) not in all_api_list:
|
||||
delete_api.append((api.method, api.path))
|
||||
for item in delete_api:
|
||||
method, path = item
|
||||
logger.debug(f"API Deleted {method} {path}")
|
||||
await Api.filter(method=method, path=path).delete()
|
||||
|
||||
for route in app.routes:
|
||||
if isinstance(route, APIRoute) and len(route.dependencies) > 0:
|
||||
method = list(route.methods)[0]
|
||||
path = route.path_format
|
||||
summary = route.summary
|
||||
tags = list(route.tags)[0]
|
||||
api_obj = await Api.filter(method=method, path=path).first()
|
||||
if api_obj:
|
||||
await api_obj.update_from_dict(dict(method=method, path=path, summary=summary, tags=tags)).save()
|
||||
else:
|
||||
logger.debug(f"API Created {method} {path}")
|
||||
await Api.create(**dict(method=method, path=path, summary=summary, tags=tags))
|
||||
|
||||
|
||||
api_controller = ApiController()
|
||||
@@ -0,0 +1,82 @@
|
||||
# app/controllers/automation.py (继续追加)
|
||||
from app.core.crud import CRUDBase
|
||||
from typing import List, Optional, Dict, Any
|
||||
from app.models.automation import Action
|
||||
from app.models.enums import ActionType
|
||||
from app.schemas.automation import ActionCreate, ActionUpdate
|
||||
from app.utils.common import transform_pydantic_to_list
|
||||
from app.schemas.automation import NotifyAction
|
||||
|
||||
class ActionController(CRUDBase[Action, ActionCreate, ActionUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Action)
|
||||
|
||||
async def get_pending_actions_by_task(self, task_id: int) -> List[Action]:
|
||||
"""获取某任务中未完成的动作"""
|
||||
return await self.model.filter(task_id=task_id, done=False).all()
|
||||
|
||||
async def list_automation_actions(self) -> List[Dict[str, Any]]:
|
||||
"""获取动作类型映射"""
|
||||
|
||||
ActionTypeMap = [
|
||||
{"value": ActionType.CREATE_GROUP, "label": "创建群聊", "automation": True, "allowAutoExecute": False, "autoExecute": False, "enable": True},
|
||||
{"value": ActionType.VIEW_ERP_LOG, "label": "查看ERP日志", "automation": False, "allowAutoExecute": False, "autoExecute": False, "enable": True},
|
||||
{"value": ActionType.BIND_USER, "label": "绑定用户", "automation": False, "allowAutoExecute": False, "autoExecute": False, "enable": True},
|
||||
{"value": ActionType.BIND_ORDER, "label": "绑定订单", "automation": False, "allowAutoExecute": False, "autoExecute": False, "enable": True},
|
||||
{"value": ActionType.WRITE_REMARK, "label": "写ERP额外备注", "automation": True, "allowAutoExecute": True, "autoExecute": True, "enable": True},
|
||||
{"value": ActionType.SEND_WECHAT_NOTIFY, "label": "发送通知", "automation": True, "allowAutoExecute": True, "autoExecute": True, "enable": True, "schema": transform_pydantic_to_list(NotifyAction)},
|
||||
{"value": ActionType.CLEAN_GROUP, "label": "清理群聊", "automation": True, "allowAutoExecute": False, "autoExecute": True, "enable": True},
|
||||
{"value": ActionType.SET_GROUP_ADMIN, "label": "设置群管理员", "automation": True, "allowAutoExecute": False, "autoExecute": True, "enable": True},
|
||||
]
|
||||
|
||||
return [item for item in ActionTypeMap if item["automation"]]
|
||||
|
||||
async def mark_action_done(
|
||||
self,
|
||||
action_id: int,
|
||||
userid: str,
|
||||
username: str,
|
||||
result: Dict[str, Any],
|
||||
notes: Optional[str] = None
|
||||
) -> Action:
|
||||
"""标记动作为已完成"""
|
||||
update_data = ActionUpdate(
|
||||
done=True,
|
||||
done_at=self.model._meta.db_fields.get("done_at").to_db_value(None, None), # 实际用 datetime.now()
|
||||
userid=userid,
|
||||
username=username,
|
||||
result=result,
|
||||
notes=notes
|
||||
)
|
||||
# Note: 建议在 service 层处理 done_at = datetime.utcnow()
|
||||
return await self.update(action_id, update_data)
|
||||
|
||||
async def create_actions_for_task(self, task_id: int, actions_def: List[Dict]) -> List[Action]:
|
||||
"""为任务批量创建动作实例"""
|
||||
actions = []
|
||||
for act in actions_def:
|
||||
action_in = ActionCreate(
|
||||
task_id=task_id,
|
||||
type=act.get("type"),
|
||||
detail=act.get("detail", {}),
|
||||
done=False
|
||||
)
|
||||
action = await self.create(action_in)
|
||||
actions.append(action)
|
||||
return actions
|
||||
|
||||
async def trigger_scenarios(self, scenario_list: List[Action], event_data: Dict[str, Any]) -> None:
|
||||
"""触发场景执行"""
|
||||
for scenario in scenario_list:
|
||||
if not scenario.enabled:
|
||||
continue
|
||||
|
||||
for action in scenario.actions:
|
||||
if not action.enabled:
|
||||
continue
|
||||
|
||||
if action.type == ActionType.SEND_WECHAT_NOTIFY:
|
||||
await self.trigger_scenarios_by_action(action, event_data)
|
||||
|
||||
|
||||
action_controller = ActionController()
|
||||
@@ -0,0 +1,222 @@
|
||||
# app/controllers/automation.py
|
||||
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
from app.core.crud import CRUDBase
|
||||
from app.models.automation import Scenario, ScenarioTriggerIndex, ScenarioScope
|
||||
from app.schemas.automation import ScenarioCreate, ScenarioUpdate
|
||||
from app.utils.event_task import event_manager
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ScenarioController(CRUDBase[Scenario, ScenarioCreate, ScenarioUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Scenario)
|
||||
|
||||
async def update_trigger_index(self, scenario: Scenario, user_id: str) -> Scenario:
|
||||
"""更新场景"""
|
||||
add_list = []
|
||||
event_set = set()
|
||||
for condition in scenario.trigger.get('conditions') or []:
|
||||
event_name = condition.get('event_name', '')
|
||||
if event_name and event_name in event_set:
|
||||
continue
|
||||
event_set.add(event_name)
|
||||
|
||||
orm = ScenarioTriggerIndex(
|
||||
owner_user_id=user_id,
|
||||
scenario_id=scenario.id,
|
||||
is_global=scenario.is_global,
|
||||
event_name=condition.get('event_name', ''),
|
||||
enabled=condition.get('enabled', True),
|
||||
scope=condition.get('scope', ScenarioScope.PERSONAL),
|
||||
)
|
||||
add_list.append(orm)
|
||||
|
||||
if add_list:
|
||||
await ScenarioTriggerIndex.bulk_create(add_list)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def new_scenario(self, scenario_in: ScenarioCreate, user_id: str) -> Scenario:
|
||||
"""创建新场景"""
|
||||
obj = await self.create(scenario_in)
|
||||
|
||||
# 更新触发条件
|
||||
if obj.trigger:
|
||||
await self.update_trigger_index(obj, user_id)
|
||||
|
||||
return obj
|
||||
|
||||
async def update_scenario(self, scenario_in: ScenarioUpdate, user_id: str) -> Scenario:
|
||||
"""更新场景"""
|
||||
obj = await self.update(id=scenario_in.id, obj_in=scenario_in)
|
||||
|
||||
# 更新触发条件
|
||||
if obj.trigger:
|
||||
await ScenarioTriggerIndex.filter(scenario_id=obj.id).delete()
|
||||
await self.update_trigger_index(obj, user_id)
|
||||
|
||||
return obj
|
||||
|
||||
async def remove_scenario(self, scenario_id: int) -> None:
|
||||
"""删除场景"""
|
||||
await self.model.filter(id=scenario_id).delete()
|
||||
await ScenarioTriggerIndex.filter(scenario_id=scenario_id).delete()
|
||||
return True
|
||||
|
||||
async def list_automation_events(self) -> List[Dict[str, Any]]:
|
||||
"""获取所有自动化事件"""
|
||||
return [a for a in event_manager.automation_event_handlers if a.get("automation_event")]
|
||||
|
||||
async def get_global_scenarios(self, enabled: bool = True) -> List[Scenario]:
|
||||
"""获取所有启用的全局场景"""
|
||||
return await self.model.filter(is_global=True, enabled=enabled).all()
|
||||
|
||||
async def get_user_scenarios(self, user_id: str, enabled: bool = True) -> List[Scenario]:
|
||||
"""获取某用户的启用个人场景"""
|
||||
return await self.model.filter(owner_user_id=user_id, is_global=False, enabled=enabled).all()
|
||||
|
||||
async def get_applicable_scenarios(self, user_id: str, enabled: bool = True) -> List[Scenario]:
|
||||
"""获取对某用户生效的所有场景(全局 + 个人)"""
|
||||
global_scenarios = await self.get_global_scenarios(enabled=enabled)
|
||||
personal_scenarios = await self.get_user_scenarios(user_id, enabled=enabled)
|
||||
return global_scenarios + personal_scenarios
|
||||
|
||||
|
||||
class AutomationScenarioController(ScenarioController):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def find_active_scenarios(self, event_name: str, event_data: Dict[str, Any]) -> List[Scenario]:
|
||||
"""查找所有实际激活的场景配置"""
|
||||
scenario_index_list = await ScenarioTriggerIndex.filter(enabled=True, event_name=event_name).all()
|
||||
|
||||
scenario_list = []
|
||||
if scenario_index_list:
|
||||
scenario_list = await self.model.filter(id__in=[s.scenario_id for s in scenario_index_list]).all()
|
||||
|
||||
logger.info(f'find_active_scenarios: {len(scenario_list)}')
|
||||
|
||||
active_scenarios = []
|
||||
for scenario in scenario_list:
|
||||
result, reason = await self.check_is_applicable(scenario, event_data)
|
||||
if result:
|
||||
active_scenarios.append((scenario, reason))
|
||||
|
||||
return active_scenarios
|
||||
|
||||
async def check_is_applicable(self, scenario: Scenario, event_data: Dict[str, Any]) -> tuple[bool, str]:
|
||||
"""检查场景是否适用于当前事件"""
|
||||
|
||||
trigger = scenario.trigger
|
||||
# 如果没有条件,默认返回True,即只要事件发生就会激活场景
|
||||
if not trigger or not trigger.get('conditions'):
|
||||
logger.info(f'scenario {scenario.id} trigger without conditions, always return True')
|
||||
return True, '没有设置具体的触发条件,事件发生就激活场景'
|
||||
|
||||
logic = trigger.get('logic', 'and')
|
||||
conditions = trigger.get('conditions', [])
|
||||
|
||||
if logic == 'or':
|
||||
for condition in conditions:
|
||||
condition = condition.get('condition', {})
|
||||
|
||||
result, reason = self.check_condition(condition, event_data)
|
||||
if result:
|
||||
logger.info(f'scenario {scenario.id} trigger condition {condition} return True, reason: {reason}')
|
||||
return True, reason
|
||||
return False, '所有触发条件都不满足'
|
||||
elif logic == 'and':
|
||||
|
||||
reason_list = []
|
||||
for condition in conditions:
|
||||
condition = condition.get('condition', {})
|
||||
|
||||
result, reason = self.check_condition(condition, event_data)
|
||||
if not result:
|
||||
logger.info(f'scenario {scenario.id} trigger condition {condition} return False, reason: {reason}')
|
||||
return False, f'触发条件 {condition} 不满足, 原因: {reason}'
|
||||
else:
|
||||
reason_list.append(reason)
|
||||
|
||||
return True, f'{", ".join(reason_list)}'
|
||||
|
||||
raise ValueError(f"Invalid logic operator: {logic}")
|
||||
|
||||
def check_condition(self, condition: Dict[str, Any], event_data: Dict[str, Any]) -> tuple[bool, str]:
|
||||
"""检查单个条件是否满足"""
|
||||
def find_field_val(field: str, event_data: Dict[str, Any]) -> Any:
|
||||
"""递归查找字段值"""
|
||||
if '.' in field:
|
||||
parts = field.split('.')
|
||||
current = event_data
|
||||
for part in parts:
|
||||
if isinstance(current, list):
|
||||
# current = current[int(part)]
|
||||
result = []
|
||||
for item in current:
|
||||
# find_field_val
|
||||
field_val = find_field_val(part, item)
|
||||
if field_val is not None:
|
||||
result.append(field_val)
|
||||
return result
|
||||
elif isinstance(current, dict):
|
||||
current = current.get(part, None)
|
||||
else:
|
||||
return None
|
||||
return current
|
||||
return event_data.get(field, None)
|
||||
|
||||
if not condition or not condition.get('field', ''):
|
||||
logger.error(f'scenario check_condition: field is empty, 当做无效条件处理')
|
||||
return True, '没有设置具体的触发条件,事件发生就激活场景'
|
||||
|
||||
field = condition.get('field', '')
|
||||
|
||||
value = condition.get('value', '')
|
||||
operator = condition.get('operator', '')
|
||||
field_val = find_field_val(field, event_data)
|
||||
if field_val is None:
|
||||
logger.error(f'scenario check_condition: field {field} value is None, event_data: {event_data}')
|
||||
return False, f'触发条件 {condition} 字段 {field} 不存在'
|
||||
|
||||
# 字符串比较
|
||||
if operator in ['contains', 'not_contains']:
|
||||
# if not isinstance(field_val, str):
|
||||
# return False, f'字段 {field} 不是字符串类型'
|
||||
if operator == 'contains' and value in field_val:
|
||||
return True, f'字段 {field} 包含 {value}'
|
||||
if operator == 'not_contains' and value not in field_val:
|
||||
return True, f'字段 {field} 不包含 {value}'
|
||||
return False, f'字段 {field} 不满足 {operator} {value}'
|
||||
|
||||
# 数字、时间比较
|
||||
if operator in ['gt', 'gte', 'lt', 'lte', 'eq', 'ne']:
|
||||
if not isinstance(field_val, (int, float, str)):
|
||||
return False, f'字段 {field} 不是数字或时间类型'
|
||||
try:
|
||||
field_val = float(field_val)
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
return False, f'字段 {field} 不是数字或时间类型'
|
||||
|
||||
if operator == 'gt' and field_val > value:
|
||||
return True, f'字段 {field} 大于 {value}'
|
||||
if operator == 'gte' and field_val >= value:
|
||||
return True, f'字段 {field} 大于等于 {value}'
|
||||
if operator == 'lt' and field_val < value:
|
||||
return True, f'字段 {field} 小于 {value}'
|
||||
if operator == 'lte' and field_val <= value:
|
||||
return True, f'字段 {field} 小于等于 {value}'
|
||||
if operator == 'eq' and field_val == value:
|
||||
return True, f'字段 {field} 等于 {value}'
|
||||
if operator == 'ne' and field_val != value:
|
||||
return True, f'字段 {field} 不等于 {value}'
|
||||
|
||||
return False, f'字段 {field} 不满足 {operator} {value}'
|
||||
|
||||
|
||||
|
||||
automation_scenario_controller = AutomationScenarioController()
|
||||
@@ -0,0 +1,77 @@
|
||||
# app/controllers/automation.py (继续追加)
|
||||
from app.core.crud import CRUDBase
|
||||
from typing import List, Optional, Dict, Any, Tuple
|
||||
|
||||
from tortoise.expressions import Q
|
||||
from app.models.automation import Task, Scenario, ScenarioScope
|
||||
from app.schemas.automation import TaskCreate, TaskUpdate
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class TaskController(CRUDBase[Task, TaskCreate, TaskUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Task)
|
||||
|
||||
async def get_user_tasks(
|
||||
self,
|
||||
user_id: str,
|
||||
status: Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20
|
||||
) -> Tuple[int, List[Task]]:
|
||||
"""分页获取某用户的待办(支持按状态过滤)"""
|
||||
query = Q(assignee_user_id=user_id)
|
||||
if status:
|
||||
query &= Q(status=status)
|
||||
return await self.list(page=page, page_size=page_size, search=query, order=["-created_at"])
|
||||
|
||||
async def get_tasks_by_customer(self, customer_id: str) -> List[Task]:
|
||||
"""获取与某客户相关的所有待办(用于侧边栏上下文)"""
|
||||
return await self.model.filter(related_customer_id=customer_id).order_by("-created_at").all()
|
||||
|
||||
async def create_from_scenario(
|
||||
self,
|
||||
scenario: Scenario,
|
||||
event_data: Optional[Dict[str, Any]] = None,
|
||||
assignee_user_id: Optional[str] = None,
|
||||
assignee_username: Optional[str] = None,
|
||||
reason: Optional[str] = None,
|
||||
) -> Task:
|
||||
"""根据场景模板创建待办任务"""
|
||||
# 计算截止时间(示例:场景中定义的 due_days 天后)
|
||||
due_at = scenario.due_days and (datetime.now() + timedelta(days=scenario.due_days)) or None
|
||||
|
||||
owner_user_id = None
|
||||
if scenario.scope == ScenarioScope.ALL:
|
||||
owner_user_id = ScenarioScope.ALL
|
||||
elif scenario.scope == ScenarioScope.PERSONAL:
|
||||
owner_user_id = scenario.owner_user_id
|
||||
|
||||
task_in = TaskCreate(
|
||||
title=scenario.title,
|
||||
reason=reason,
|
||||
notes=scenario.notes,
|
||||
event_data=event_data or {},
|
||||
source_scenario=scenario,
|
||||
source_scenario_id=scenario.id,
|
||||
owner_user_id=owner_user_id,
|
||||
assignee_user_id=assignee_user_id,
|
||||
assignee_username=assignee_username,
|
||||
due_at=due_at,
|
||||
status="pending",
|
||||
auto_closeable=True,
|
||||
)
|
||||
return await self.create(task_in)
|
||||
|
||||
async def auto_close_by_event(self, event_type: str, payload: Dict[str, Any]) -> List[Task]:
|
||||
"""
|
||||
根据事件自动关闭匹配的待办(用于自动消除)
|
||||
例如:当 group_id 匹配且动作含 create_group 时,关闭待办
|
||||
"""
|
||||
# 示例逻辑:后续可扩展为规则引擎
|
||||
closed_tasks = []
|
||||
# 这里可遍历 pending 任务,检查 content 中的动作是否已被事件满足
|
||||
# 为简化,此处留作扩展点
|
||||
return closed_tasks
|
||||
|
||||
|
||||
task_controller = TaskController()
|
||||
@@ -0,0 +1,235 @@
|
||||
import asyncio
|
||||
from typing import List, Dict, Any
|
||||
|
||||
import concurrent # 添加这行
|
||||
import concurrent.futures
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.schemas.crm import (
|
||||
CrmCustomerCreate,
|
||||
CrmCustomerUpdate,
|
||||
CrmCustomerCreate,
|
||||
CrmBindInfoCreate,
|
||||
CrmBindInfoUpdate,
|
||||
)
|
||||
|
||||
from some_sdk.services import binder as binder_service
|
||||
from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user
|
||||
from app.models.weixin import CrmCustomer, CrmBindInfo
|
||||
from tortoise.expressions import Subquery
|
||||
from tortoise.functions import Count # 导入 Count 函数
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def split_generator(iterable, chunk_size=50):
|
||||
"""
|
||||
将可迭代对象拆分为多个子列表
|
||||
"""
|
||||
chunk = []
|
||||
for item in iterable:
|
||||
chunk.append(item)
|
||||
if len(chunk) == chunk_size:
|
||||
yield chunk
|
||||
chunk = []
|
||||
if chunk:
|
||||
yield chunk
|
||||
|
||||
class CrmCustomerController(CRUDBase[CrmCustomer, CrmCustomerCreate, CrmCustomerUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=CrmCustomer)
|
||||
|
||||
async def get_unbound_customers_method_four_alt(self, limit: int = 50):
|
||||
# 创建子查询
|
||||
bound_ids_subquery = CrmBindInfo.all().values_list('customer_id', flat=True)
|
||||
|
||||
# 使用正确的子查询语法
|
||||
unbound_customers = await self.model.filter(
|
||||
cid__not_in=Subquery(bound_ids_subquery)
|
||||
).limit(limit)
|
||||
return unbound_customers
|
||||
|
||||
async def bind_crm_all_customer_by_db(self):
|
||||
|
||||
# 定义一个函数来执行单个同步的 bind_shop_info 调用
|
||||
def sync_fetch_bind_info(user_data: Dict[str, Any]):
|
||||
# 同步调用 binder_service
|
||||
all_users = []
|
||||
for crm_user, user_list_from_bind in binder_service.bind_shop_info([user_data]):
|
||||
all_users.extend(user_list_from_bind)
|
||||
return all_users
|
||||
|
||||
parsed_set = set()
|
||||
while True:
|
||||
# 获取一批未绑定的用户 ORM 对象
|
||||
unbound_orm_objects = await self.get_unbound_customers_method_four_alt(limit=50)
|
||||
if not unbound_orm_objects:
|
||||
break
|
||||
print(f'获取到 {len(unbound_orm_objects)} 个待绑定的 ORM 对象', flush=True)
|
||||
expected_cids = {obj.cid for obj in unbound_orm_objects if obj.cid is not None and obj.cid not in parsed_set}
|
||||
if not expected_cids: break
|
||||
user_data_list = [await obj.to_dict() for obj in unbound_orm_objects if obj.cid not in parsed_set]
|
||||
[parsed_set.add(cid) for cid in expected_cids]
|
||||
print(f'待绑定用户数(转换后):{len(user_data_list)}', flush=True)
|
||||
await self._bind_crm_all_customer_by_db(user_data_list, expected_cids, sync_fetch_bind_info)
|
||||
|
||||
async def _bind_crm_all_customer_by_db(self, not_in_db_list: List[Dict[str, Any]], expected_cids: set, sync_fetch_bind_info):
|
||||
print(f'待绑定用户数(传入列表):{len(not_in_db_list)}', flush=True)
|
||||
|
||||
# --- 并发执行所有同步的 bind_shop_info 调用 ---
|
||||
# 使用 asyncio.to_thread (Python 3.9+) 或 run_in_executor 将同步函数移到线程池执行
|
||||
# 限制并发线程数很重要,避免创建过多线程
|
||||
max_workers = 10 # 限制线程池大小,根据系统性能调整
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# 创建任务列表
|
||||
tasks = []
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
for user_data in not_in_db_list:
|
||||
# 将同步函数提交到线程池执行,并返回一个 Future
|
||||
# asyncio.run_in_executor 将 Future 包装成 awaitable 的协程
|
||||
task = loop.run_in_executor(executor, sync_fetch_bind_info, user_data)
|
||||
tasks.append(task)
|
||||
|
||||
# 等待所有线程池任务完成
|
||||
all_bind_lists_results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# 处理结果,将异常和正常结果分开
|
||||
all_users_to_process = []
|
||||
for result in all_bind_lists_results:
|
||||
if isinstance(result, Exception):
|
||||
logger.exception(f"Thread pool task failed: {result}")
|
||||
# 可以选择跳过或记录错误
|
||||
else:
|
||||
all_users_to_process.extend(result)
|
||||
|
||||
# --- 去重逻辑 ---
|
||||
unique_users_to_process = {}
|
||||
for user in all_users_to_process:
|
||||
customer_id = user.get("customer_id", 0)
|
||||
tenant_name = user.get("tenant_name", 0)
|
||||
platform = user.get("platform", 0)
|
||||
|
||||
key = f"{customer_id}_{tenant_name}_{platform}"
|
||||
if key not in unique_users_to_process:
|
||||
unique_users_to_process[key] = user
|
||||
|
||||
print(f"去重后待处理用户数: {len(unique_users_to_process)}", flush=True)
|
||||
|
||||
# --- 并发处理去重后的用户 ---
|
||||
# 这部分可以保持原有逻辑,因为它已经是并发的了
|
||||
# 但要注意,如果同时有多个任务尝试插入相同的 customer_id,可能会有并发问题
|
||||
# 可以考虑使用数据库的 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE 等特性
|
||||
|
||||
async def process_user(user_obj: Dict[str, Any]):
|
||||
try:
|
||||
user_type = user_obj.get("type", 0)
|
||||
tenant_name = user_obj.get("tenant_name", 0)
|
||||
customer_id = user_obj.get("customer_id", 0)
|
||||
platform = user_obj.get("platform", 0)
|
||||
|
||||
if user_type == 'staff':
|
||||
print(f'绑定员工:{user_obj}', flush=True)
|
||||
else:
|
||||
print(f'绑定客户:{user_obj}, Customer ID: {customer_id}', flush=True)
|
||||
# --- 检查数据库中是否已存在绑定 ---
|
||||
existing_bind = await CrmBindInfo.filter(customer_id=customer_id, platform=platform, tenant_name=tenant_name).first()
|
||||
if not existing_bind:
|
||||
bind_instance = CrmBindInfo.create_bind(user_obj)
|
||||
await bind_instance.save()
|
||||
print(f"客户 {customer_id} 绑定成功", flush=True)
|
||||
else:
|
||||
print(f"客户 {customer_id} 已存在绑定", flush=True)
|
||||
logger.warning(f"Attempted to bind customer_id {customer_id} which already exists in CrmBindInfo.")
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
print(f"处理用户 {user_obj.get('customer_id', 'unknown')} 时出错: {e}", flush=True)
|
||||
|
||||
max_concurrent_db_tasks = 10 # 可以独立控制数据库操作的并发数
|
||||
semaphore = asyncio.Semaphore(max_concurrent_db_tasks)
|
||||
|
||||
async def process_user_with_semaphore(user_obj):
|
||||
async with semaphore:
|
||||
return await process_user(user_obj)
|
||||
|
||||
tasks = [process_user_with_semaphore(user_obj) for user_obj in unique_users_to_process.values()]
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
print("当前批次所有用户绑定任务完成", flush=True)
|
||||
|
||||
async def bind_crm_all_customer(self):
|
||||
"""
|
||||
绑定CRM客户和微信用户
|
||||
"""
|
||||
|
||||
await self.bind_crm_all_customer_by_db()
|
||||
|
||||
for shop in binder_service.iter_shop():
|
||||
shopId = shop.get("shopId", "")
|
||||
shopName = shop.get("shopName", "")
|
||||
print(f"开始处理店铺:{shopName}", flush=True)
|
||||
|
||||
not_in_db_list = []
|
||||
|
||||
shop_info = {
|
||||
'shopId': shopId,
|
||||
'shopName': shopName,
|
||||
}
|
||||
|
||||
is_done = False
|
||||
user_list_iter = binder_service.iter_list_trade_user(binder_service.xy_client, shopId=shopId)
|
||||
for user_list in split_generator(user_list_iter):
|
||||
if is_done: break
|
||||
# 提取所有待检查的 cid
|
||||
all_cids = [str(user.get("cid")) for user in user_list]
|
||||
# print(user_list[0])
|
||||
# break
|
||||
|
||||
# 只查询 user_list 中存在的 cid
|
||||
in_objs = await self.model.filter(cid__in=all_cids)
|
||||
|
||||
# 提取已存在的 cid
|
||||
in_db_cid_set = {user.cid for user in in_objs}
|
||||
|
||||
print(f'已处理用户数:{len(in_db_cid_set)}', flush=True)
|
||||
|
||||
# 筛选待处理用户
|
||||
not_in_db_list = [user for user in user_list if user.get("cid") not in in_db_cid_set]
|
||||
for user in not_in_db_list:
|
||||
id = user.pop("id", None)
|
||||
# print('user', user)
|
||||
|
||||
# if not not_in_db_list:
|
||||
# is_done = True
|
||||
# break
|
||||
|
||||
print(f'待处理用户数:{len(not_in_db_list)}', flush=True)
|
||||
|
||||
if not_in_db_list:
|
||||
model_list = [self.model(**CrmCustomerCreate(**user).model_dump(exclude_unset=True)) for user in not_in_db_list]
|
||||
await self.model.bulk_create(model_list)
|
||||
|
||||
if not not_in_db_list:
|
||||
print(f'店铺:{shopName},无待处理用户', flush=True)
|
||||
continue
|
||||
|
||||
await self.bind_crm_all_customer_by_db()
|
||||
|
||||
async def bind_user(self, bind_info: CrmBindInfoCreate):
|
||||
"""
|
||||
绑定用户
|
||||
"""
|
||||
bind_info_dict = bind_info.model_dump(exclude_unset=True)
|
||||
platform = bind_info_dict.get("platform", None)
|
||||
platform_id = bind_info_dict.get("platform_id", None)
|
||||
bind_info_dict.pop("id", None)
|
||||
|
||||
existing_bind = await CrmBindInfo.filter(platform=platform, platform_id=platform_id).first()
|
||||
if existing_bind:
|
||||
await CrmBindInfo.filter(id=existing_bind.id).update(**bind_info_dict)
|
||||
else:
|
||||
obj = CrmBindInfo(**bind_info_dict)
|
||||
await obj.save()
|
||||
return {"message": "用户绑定成功"}
|
||||
|
||||
crm_customer_controller = CrmCustomerController()
|
||||
@@ -0,0 +1,41 @@
|
||||
from typing import List
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.models.admin import Api, Menu, Datasource
|
||||
from app.schemas.codegen import DatasourceCreate, DatasourceUpdate, DatasourceInfo
|
||||
from app.utils.db import DatabaseInfo
|
||||
|
||||
class DatasourceController(CRUDBase[Datasource, DatasourceCreate, DatasourceUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Datasource)
|
||||
|
||||
async def load_tables(self, name: str) -> list[DatasourceInfo]:
|
||||
# datasource_obj = await self.model.filter(name=name).first()
|
||||
# if not datasource_obj:
|
||||
# raise HTTPException(status_code=400, detail="数据源不存在")
|
||||
|
||||
with DatabaseInfo(
|
||||
host="lt.330770.xyz",
|
||||
port=3307,
|
||||
user="root",
|
||||
password="rap_sky",
|
||||
database="rpa"
|
||||
) as db:
|
||||
tables = db.get_all_tables()
|
||||
|
||||
return tables
|
||||
|
||||
|
||||
async def update_datasources(self, datasource: Datasource, menu_ids: List[int], api_infos: List[dict]) -> None:
|
||||
await datasource.menus.clear()
|
||||
for menu_id in menu_ids:
|
||||
menu_obj = await Menu.filter(id=menu_id).first()
|
||||
await datasource.menus.add(menu_obj)
|
||||
|
||||
await datasource.apis.clear()
|
||||
for item in api_infos:
|
||||
api_obj = await Api.filter(path=item.get("path"), method=item.get("method")).first()
|
||||
await datasource.apis.add(api_obj)
|
||||
|
||||
|
||||
datasource_controller = DatasourceController()
|
||||
@@ -0,0 +1,86 @@
|
||||
from tortoise.expressions import Q
|
||||
from tortoise.transactions import atomic
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.models.admin import Dept, DeptClosure
|
||||
from app.schemas.depts import DeptCreate, DeptUpdate
|
||||
|
||||
|
||||
class DeptController(CRUDBase[Dept, DeptCreate, DeptUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Dept)
|
||||
|
||||
async def get_dept_tree(self, name):
|
||||
q = Q()
|
||||
# 获取所有未被软删除的部门
|
||||
q &= Q(is_deleted=False)
|
||||
if name:
|
||||
q &= Q(name__contains=name)
|
||||
all_depts = await self.model.filter(q).order_by("order")
|
||||
|
||||
# 辅助函数,用于递归构建部门树
|
||||
def build_tree(parent_id):
|
||||
return [
|
||||
{
|
||||
"id": dept.id,
|
||||
"name": dept.name,
|
||||
"desc": dept.desc,
|
||||
"order": dept.order,
|
||||
"parent_id": dept.parent_id,
|
||||
"children": build_tree(dept.id), # 递归构建子部门
|
||||
}
|
||||
for dept in all_depts
|
||||
if dept.parent_id == parent_id
|
||||
]
|
||||
|
||||
# 从顶级部门(parent_id=0)开始构建部门树
|
||||
dept_tree = build_tree(0)
|
||||
return dept_tree
|
||||
|
||||
async def get_dept_info(self):
|
||||
pass
|
||||
|
||||
async def update_dept_closure(self, obj: Dept):
|
||||
parent_depts = await DeptClosure.filter(descendant=obj.parent_id)
|
||||
for i in parent_depts:
|
||||
print(i.ancestor, i.descendant)
|
||||
dept_closure_objs: list[DeptClosure] = []
|
||||
# 插入父级关系
|
||||
for item in parent_depts:
|
||||
dept_closure_objs.append(DeptClosure(ancestor=item.ancestor, descendant=obj.id, level=item.level + 1))
|
||||
# 插入自身x
|
||||
dept_closure_objs.append(DeptClosure(ancestor=obj.id, descendant=obj.id, level=0))
|
||||
# 创建关系
|
||||
await DeptClosure.bulk_create(dept_closure_objs)
|
||||
|
||||
@atomic()
|
||||
async def create_dept(self, obj_in: DeptCreate):
|
||||
# 创建
|
||||
if obj_in.parent_id != 0:
|
||||
await self.get(id=obj_in.parent_id)
|
||||
new_obj = await self.create(obj_in=obj_in)
|
||||
await self.update_dept_closure(new_obj)
|
||||
|
||||
@atomic()
|
||||
async def update_dept(self, obj_in: DeptUpdate):
|
||||
dept_obj = await self.get(id=obj_in.id)
|
||||
# 更新部门关系
|
||||
if dept_obj.parent_id != obj_in.parent_id:
|
||||
await DeptClosure.filter(ancestor=dept_obj.id).delete()
|
||||
await DeptClosure.filter(descendant=dept_obj.id).delete()
|
||||
await self.update_dept_closure(dept_obj)
|
||||
# 更新部门信息
|
||||
dept_obj.update_from_dict(obj_in.model_dump(exclude_unset=True))
|
||||
await dept_obj.save()
|
||||
|
||||
@atomic()
|
||||
async def delete_dept(self, dept_id: int):
|
||||
# 删除部门
|
||||
obj = await self.get(id=dept_id)
|
||||
obj.is_deleted = True
|
||||
await obj.save()
|
||||
# 删除关系
|
||||
await DeptClosure.filter(descendant=dept_id).delete()
|
||||
|
||||
|
||||
dept_controller = DeptController()
|
||||
@@ -0,0 +1,43 @@
|
||||
from .finance_parse import parse_finance_data
|
||||
from tortoise.expressions import Q
|
||||
from tortoise.transactions import atomic
|
||||
|
||||
from app.models.automation import Task, TaskStatus
|
||||
from app.schemas.task import TaskModel, DecodeTaskParams, DecodeTaskResult
|
||||
|
||||
class TaskController(object):
|
||||
async def get_task(self, name, type):
|
||||
q = Q()
|
||||
# 获取所有未被软删除的任务
|
||||
q &= Q(is_deleted=False)
|
||||
if name: q &= Q(name__contains=name)
|
||||
if type: q &= Q(type=type)
|
||||
all_tasks = await self.model.filter(q).order_by("id")
|
||||
return all_tasks
|
||||
|
||||
async def list(self, name, type, page: int = 1, page_size: int = 10, order: list[str] = ["id"]):
|
||||
q = Q()
|
||||
if name: q &= Q(name__contains=name)
|
||||
if type: q &= Q(type=type)
|
||||
query = Task.filter(q)
|
||||
return await query.count(), await query.offset((page - 1) * page_size).limit(page_size).order_by(*order)
|
||||
|
||||
@atomic()
|
||||
async def create_task(self, name: str, obj_in: DecodeTaskParams):
|
||||
task_obj = TaskModel.create_decode_task(name, obj_in)
|
||||
task_obj.result = task_obj.result or {}
|
||||
obj = Task(**task_obj.model_dump())
|
||||
await obj.save()
|
||||
return obj, task_obj
|
||||
|
||||
@atomic()
|
||||
async def update_task(self, obj_in: TaskModel, task_id: int, status: TaskStatus, result: DecodeTaskResult):
|
||||
task_obj = await Task.get(id=task_id)
|
||||
obj_in.status = status
|
||||
obj_in.set_result(result)
|
||||
|
||||
# 更新任务信息
|
||||
task_obj.update_from_dict(obj_in.model_dump(exclude_unset=True))
|
||||
await task_obj.save()
|
||||
|
||||
task_controller = TaskController()
|
||||
@@ -0,0 +1,616 @@
|
||||
# spacy_training/scripts/predict.py
|
||||
import re
|
||||
import cn2an
|
||||
import os.path
|
||||
from tqdm import tqdm
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# 单位与连接符定义
|
||||
MAX_ITEM_OFFSET = 10
|
||||
UNIT_PATTERN = "个|张|片|块|条|幅|根"
|
||||
SIZE_UNITS_PATTERN = r'mm|cm|dm|m'
|
||||
SIZE_JOIN_PATTERN = "xX×"
|
||||
NUMS_PATTERN = r'\d+(?:\.\d+)' # 修复:原缺少 ? 导致整数不匹配
|
||||
CNT_PATTERN = rf'{NUMS_PATTERN}?[^克+\s\d-]*'
|
||||
EXCEPT_TEXT = "解析失败"
|
||||
INDEX_COL = '序号'
|
||||
|
||||
base_colume = ['解析备注', '尺寸备注', '描述', '异常信息']
|
||||
mapping_size = {
|
||||
"width_mm": "长",
|
||||
"height_mm": "宽",
|
||||
# "original_size": "原始尺寸",
|
||||
# "total_quantity": "总数量",
|
||||
"style_count": "款数",
|
||||
"quantity_per_style": "数量",
|
||||
"unit": "单位",
|
||||
"描述": "描述",
|
||||
"解析备注": "解析备注",
|
||||
"size_unit": "尺寸单位",
|
||||
"exception_msg": '异常信息'
|
||||
}
|
||||
|
||||
import spacy
|
||||
import pandas as pd
|
||||
|
||||
# 将 doc 转换为字典
|
||||
def doc_to_dict(doc):
|
||||
return {
|
||||
"text": doc.text,
|
||||
"entities": [
|
||||
{
|
||||
"text": ent.text,
|
||||
"label": ent.label_,
|
||||
"start": ent.start_char,
|
||||
"end": ent.end_char
|
||||
}
|
||||
for ent in doc.ents
|
||||
],
|
||||
}
|
||||
|
||||
def predict_file(df, model_path="./spacy_training/model", key='备注'):
|
||||
print(model_path)
|
||||
nlp = spacy.load(model_path)
|
||||
if "sentencizer" not in nlp.pipe_names:
|
||||
nlp.add_pipe("sentencizer")
|
||||
|
||||
# 统一表头空格
|
||||
df.columns = df.columns.str.strip()
|
||||
key = key.strip()
|
||||
|
||||
for i, row in tqdm(df.iterrows(), total=len(df)):
|
||||
order = str(row[key]).strip()
|
||||
base_row = {key: val for key, val in row.items()} # 复制原行数据
|
||||
yield order, base_row, doc_to_dict(nlp(order))
|
||||
# doc = nlp(text)
|
||||
# print(f"\n🔤 文本: {text}")
|
||||
# import json; print(json.dumps(doc_to_dict(doc), indent=4, ensure_ascii=False))
|
||||
|
||||
# break
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class StructuredOrder:
|
||||
key: str
|
||||
width_mm: float
|
||||
height_mm: float
|
||||
original_size: str
|
||||
style_count: int
|
||||
quantity_per_style: float
|
||||
unit: str
|
||||
total_quantity: float
|
||||
original_qty: str
|
||||
size_position: list
|
||||
qty_position: list
|
||||
exception_msg: str
|
||||
calc_type: str
|
||||
size_unit: str
|
||||
meta: dict
|
||||
|
||||
def expand_to_structured(raw_pairs, size_unit=''):
|
||||
unit_map = {
|
||||
'百': 100, '千': 1000, '万': 10000,
|
||||
'百万': 1000000, '千万': 10000000, '亿': 100000000,
|
||||
}
|
||||
|
||||
UNIT_REGEX = re.compile(rf'({UNIT_PATTERN})$')
|
||||
exception_msg = []
|
||||
|
||||
def extract_multiplier(text):
|
||||
"""
|
||||
从字符串中提取数值乘数,支持:
|
||||
- 阿拉伯数字 + 中文单位:3.5万 → 35000
|
||||
- 纯中文数字 + 单位:三万五千 → 35000
|
||||
- 纯中文:两千万 → 20000000
|
||||
"""
|
||||
# 去除末尾单位(保留前面的部分)
|
||||
clean = UNIT_REGEX.sub('', text.strip())
|
||||
|
||||
# 如果去单位后为空,原字符串可能是纯单位(如“万”),默认系数为1
|
||||
if not clean:
|
||||
matched_unit = UNIT_REGEX.search(text)
|
||||
if matched_unit:
|
||||
unit = matched_unit.group(1)
|
||||
return unit_map.get(unit, 1.0)
|
||||
return 1.0
|
||||
|
||||
# 尝试直接转阿拉伯数字
|
||||
try:
|
||||
return float(clean)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 尝试转中文数字(如“三万五千”虽然不合理,但“三万”可以)
|
||||
try:
|
||||
# 注意:cn2an 可以直接处理“三万五千”这种
|
||||
num = cn2an.cn2an(clean, "smart") # smart 模式支持混合写法
|
||||
if isinstance(num, (int, float)):
|
||||
# 检查原字符串是否有单位后缀(比如“三万”中的“万”已被去除,需补回)
|
||||
matched_unit = UNIT_REGEX.search(text)
|
||||
if matched_unit:
|
||||
unit = matched_unit.group(1)
|
||||
num *= unit_map[unit]
|
||||
return float(num)
|
||||
except Exception:
|
||||
exception_msg.append(f"尺寸解析异常:{text}")
|
||||
pass
|
||||
|
||||
return 1.0
|
||||
|
||||
def parse_size(size_str):
|
||||
join_str = ''.join(re.findall(f"[{SIZE_JOIN_PATTERN}]+", size_str))
|
||||
if len(join_str) == 0 or re.search('比例|等高|等比|等宽', size_str):
|
||||
result = f'异常值:{size_str}'
|
||||
return result, result, size_unit
|
||||
size_str = size_str.replace(join_str, "x")
|
||||
match = re.match(rf'({NUMS_PATTERN}?)([a-z]+)?[{SIZE_JOIN_PATTERN}]({NUMS_PATTERN}?)([a-z]+)?', size_str, re.IGNORECASE)
|
||||
if not match:
|
||||
return None, None, size_unit
|
||||
w_val, w_unit, h_val, h_unit = match.groups()
|
||||
unit = (w_unit or h_unit or size_unit).lower()
|
||||
conv = {'m': 1000, 'dm': 100, 'cm': 10, 'mm': 1}
|
||||
if unit not in conv:
|
||||
print(f'未知单位: {unit} size_str:{size_str} size_unit:{size_unit}')
|
||||
exception_msg.append(f"尺寸解析异常:{size_str}, 未知单位: {unit}")
|
||||
return None, None, unit
|
||||
w_mm = conv[unit] * float(w_val)
|
||||
h_mm = conv[unit] * float(h_val)
|
||||
return round(w_mm, 3), round(h_mm, 3), unit
|
||||
|
||||
def parse_quantity(qty_str):
|
||||
qty_str = "".join(qty_str.split())
|
||||
multi_match = re.match((
|
||||
rf'[共计]?(.+?)款\w*?([共计]?[\d.一二两三四五六七八九十百千万亿]+)({UNIT_PATTERN})'
|
||||
), qty_str, re.IGNORECASE)
|
||||
if multi_match:
|
||||
style_part, qty_part = multi_match.group(1), multi_match.group(2)
|
||||
if qty_part[0] in '共计':
|
||||
qty_part = qty_part[1:]
|
||||
calc_type = 'total'
|
||||
else:
|
||||
calc_type = 'single'
|
||||
style_count = int(extract_multiplier(style_part))
|
||||
quantity_per_style = int(extract_multiplier(qty_part))
|
||||
unit = multi_match.group(3)
|
||||
|
||||
return {"style_count": style_count, "quantity_per_style": quantity_per_style, "unit": unit, "calc_type": calc_type}
|
||||
|
||||
single_match = re.match(rf'(.+?)({UNIT_PATTERN})', qty_str, re.IGNORECASE)
|
||||
if single_match:
|
||||
qty_part = single_match.group(1)
|
||||
if qty_part[0] in '共计':
|
||||
qty_part = qty_part[1:]
|
||||
calc_type = 'total'
|
||||
else:
|
||||
calc_type = 'single'
|
||||
quantity_per_style = int(extract_multiplier(qty_part))
|
||||
unit = single_match.group(2)
|
||||
return {"style_count": 1, "quantity_per_style": quantity_per_style, "unit": unit, "calc_type": calc_type}
|
||||
|
||||
return {"style_count": 0, "quantity_per_style": 0, "unit": "个"}
|
||||
|
||||
structured = []
|
||||
|
||||
for size_info, qty_info in raw_pairs:
|
||||
s_start, s_end, s_text, *_ = size_info
|
||||
q_start, q_end, q_text, *_ = qty_info
|
||||
|
||||
if s_text == EXCEPT_TEXT:
|
||||
width_mm, height_mm = EXCEPT_TEXT, EXCEPT_TEXT
|
||||
else:
|
||||
size_parsed = parse_size(s_text)
|
||||
if size_parsed:
|
||||
width_mm, height_mm, size_unit = size_parsed
|
||||
else:
|
||||
width_mm, height_mm, size_unit = None, None, None
|
||||
|
||||
if q_text == EXCEPT_TEXT:
|
||||
style_count, quantity_per_style, unit = 0, EXCEPT_TEXT, EXCEPT_TEXT
|
||||
else:
|
||||
qty_parsed = parse_quantity(q_text)
|
||||
style_count, quantity_per_style, unit = qty_parsed["style_count"], qty_parsed["quantity_per_style"], qty_parsed["unit"]
|
||||
|
||||
structured.append(StructuredOrder(
|
||||
key=f"{s_start}{s_end}{s_text}",
|
||||
width_mm=width_mm,
|
||||
height_mm=height_mm,
|
||||
original_size=s_text,
|
||||
style_count=style_count,
|
||||
quantity_per_style=quantity_per_style,
|
||||
unit=unit,
|
||||
total_quantity=style_count * quantity_per_style if style_count and style_count else None,
|
||||
original_qty=q_text,
|
||||
size_position=[s_start, s_end],
|
||||
qty_position=[q_start, q_end],
|
||||
exception_msg="; ".join(exception_msg),
|
||||
calc_type=qty_parsed.get('calc_type', None),
|
||||
size_unit=size_unit,
|
||||
meta={
|
||||
"size_text_except": s_text if s_text == EXCEPT_TEXT or (not width_mm and not height_mm) else None,
|
||||
"qty_text_except": q_text if q_text == EXCEPT_TEXT else None,
|
||||
}
|
||||
))
|
||||
|
||||
return structured
|
||||
|
||||
|
||||
def decode(doc_dict_list, is_horizontal=False):
|
||||
ent_group = {
|
||||
# "单号": ["单号"],
|
||||
"刮刮膜尺寸": ["刮刮膜尺寸"],
|
||||
# "产品": ["产品"],
|
||||
# "材质": ["材质"],
|
||||
# "工艺": ["工艺"],
|
||||
# "用户": ["用户信息"],
|
||||
# "加急": ["加急"],
|
||||
"数量": ["尺寸", "数量"],
|
||||
}
|
||||
columns = list(ent_group.keys())
|
||||
columns.remove('数量')
|
||||
columns.extend(base_colume)
|
||||
group_reflect = {v: k for k, vs in ent_group.items() for v in vs}
|
||||
result_items = []
|
||||
exception_list = []
|
||||
|
||||
for doc_dict in doc_dict_list:
|
||||
sentence = doc_dict["text"]
|
||||
entities = doc_dict["entities"]
|
||||
|
||||
is_except = False
|
||||
|
||||
# 强关联实体需要进行组合处理
|
||||
# ======================================== 实体分组 ========================================
|
||||
ent_group_dict = {k: [] for k in ent_group.keys()}
|
||||
base_ent_group_dict = {}
|
||||
|
||||
for ent in entities:
|
||||
start, end, label, text = ent["start"], ent["end"], ent["label"], ent["text"]
|
||||
group_name = group_reflect[label]
|
||||
ent_group_dict[group_name].append((start, end, text, label))
|
||||
base_ent_group_dict[group_name] = text
|
||||
|
||||
result = []
|
||||
|
||||
# 解析出尺寸和数量的组合
|
||||
# ======================================== 解析尺寸和数量 ========================================
|
||||
group_list = []
|
||||
size_and_qty_list = [[], []]
|
||||
|
||||
# 数量需要和尺寸进行组合处理
|
||||
base_ent_group_dict.pop("数量", None)
|
||||
for cnt_item in ent_group_dict["数量"]:
|
||||
size_info, qty_info = size_and_qty_list
|
||||
label = cnt_item[-1]
|
||||
if label == '数量':
|
||||
qty_info.append(cnt_item)
|
||||
elif label == '尺寸':
|
||||
start, end, text, _label = cnt_item
|
||||
if f'-({text[:2]}' in sentence:
|
||||
continue
|
||||
if qty_info:
|
||||
group_list.append(size_and_qty_list)
|
||||
size_and_qty_list = [[], []]
|
||||
size_info, qty_info = size_and_qty_list
|
||||
size_info.append(cnt_item)
|
||||
|
||||
# 存在数量和尺寸
|
||||
if size_and_qty_list[0] or size_and_qty_list[1]:
|
||||
group_list.append(size_and_qty_list)
|
||||
|
||||
# 如果只有一对尺寸,有可能反过来描述
|
||||
if len(group_list) == 2:
|
||||
_1, qty_info = group_list[0]
|
||||
size_info, _2 = group_list[1]
|
||||
|
||||
if not _1 and not _2:
|
||||
group_list[:] = [[qty_info, size_info]]
|
||||
|
||||
# print(f'订单中尺寸和数量组合:{group_list}')
|
||||
|
||||
# 解析出尺寸和数量的组合
|
||||
# ======================================== 解析长、宽、款数、数量 ========================================
|
||||
size_unit = ''
|
||||
for size_info, qty_info in group_list:
|
||||
units = set(''.join(re.findall(SIZE_UNITS_PATTERN, ent[2])) for ent in size_info)
|
||||
units = [unit for unit in units if unit]
|
||||
if len(units) == 1:
|
||||
size_unit = units[0]
|
||||
|
||||
size_and_qty_parsed_result = []
|
||||
for size_info, qty_info in group_list:
|
||||
size_len, qty_len = len(size_info), len(qty_info)
|
||||
if size_len == 0:
|
||||
print(f'解析异常,尺寸为空,数量为{qty_info},原文:{sentence}', entities)
|
||||
is_except = True
|
||||
# 起始位置、结束位置、尺寸文本、尺寸标签
|
||||
qty_info = [(0, 0, EXCEPT_TEXT, '数量')]
|
||||
if qty_len == 0:
|
||||
print(f'解析异常,数量为空,尺寸为{size_info},原文:{sentence}', entities)
|
||||
is_except = True
|
||||
# 起始位置、结束位置、尺寸文本、尺寸标签
|
||||
size_info = [(0, 0, EXCEPT_TEXT, '尺寸')]
|
||||
# continue
|
||||
|
||||
# print('size_info, qty_info', size_info, qty_info)
|
||||
if size_len == qty_len:
|
||||
decode_desc = "单尺寸、单款式描述的订单" if size_len == 1 else "多尺寸、多款式描述一一匹配的订单"
|
||||
|
||||
for size, qty in zip(size_info, qty_info):
|
||||
expand = expand_to_structured([(size, qty)], size_unit) or []
|
||||
exceptions = [item.meta for item in expand if item.meta.get('size_text_except') or item.meta.get('qty_text_except')]
|
||||
if exceptions: print("解析异常", exceptions, '原文', sentence, entities)
|
||||
for item in expand:
|
||||
parsed_item = item.__dict__.copy()
|
||||
parsed_item['描述'] = f"{size[2]}|{qty[2]}"
|
||||
parsed_item['解析备注'] = decode_desc
|
||||
parsed_item['size_len'] = size_len
|
||||
parsed_item['qty_len'] = qty_len
|
||||
size_and_qty_parsed_result.append(parsed_item)
|
||||
elif size_len>1 and qty_len>1:
|
||||
|
||||
if size_len > qty_len:
|
||||
total_qty = []
|
||||
for size, qty in zip(size_info, qty_info):
|
||||
expand = expand_to_structured([(size, qty)], size_unit) or []
|
||||
for item in expand:
|
||||
total_qty.append(item.style_count)
|
||||
|
||||
if size_len == sum(total_qty):
|
||||
decode_desc = "多尺寸、多款式描述:多款式描述和尺寸相等的订单"
|
||||
item = []
|
||||
for idx, qty in enumerate(total_qty):
|
||||
item.extend(qty_info[idx] for _ in range(qty))
|
||||
print('解析:', size_info, qty_info, item)
|
||||
# breakpoint()
|
||||
qty_info = item
|
||||
qty_len = len(qty_info)
|
||||
else:
|
||||
# qty_info = []
|
||||
decode_desc = f"匹配异常:多尺寸、多款式描述的订单: {size_info} {qty_info}"
|
||||
else:
|
||||
# qty_info = []
|
||||
decode_desc = f"匹配异常:多尺寸、多款式描述的订单: {size_info} {qty_info}"
|
||||
|
||||
for size, qty in zip(size_info, qty_info):
|
||||
expand = expand_to_structured([(size, qty)], size_unit) or []
|
||||
exceptions = [item.meta for item in expand if item.meta.get('size_text_except') or item.meta.get('qty_text_except')]
|
||||
if exceptions: print("解析异常", exceptions, '原文', sentence, entities)
|
||||
for item in expand:
|
||||
parsed_item = item.__dict__.copy()
|
||||
parsed_item['style_count'] = 1
|
||||
parsed_item['描述'] = f"{size[2]}|{qty[2]}"
|
||||
parsed_item['解析备注'] = decode_desc
|
||||
parsed_item['size_len'] = size_len
|
||||
parsed_item['qty_len'] = qty_len
|
||||
size_and_qty_parsed_result.append(parsed_item)
|
||||
else:
|
||||
# 多尺寸-单数量、单尺寸-多数量 告警多尺寸-多数量
|
||||
iter_item = ((size, qty) for size in size_info for qty in qty_info)
|
||||
if size_len > 1 and qty_len == 1:
|
||||
decode_desc = "多尺寸、单款式描述的订单"
|
||||
elif size_len == 1 and qty_len > 1:
|
||||
decode_desc = "单尺寸、多款式描述的订单"
|
||||
else:
|
||||
decode_desc = f"异常:多尺寸、多款式描述的订单: {size_info} {qty_info}"
|
||||
|
||||
for item in iter_item:
|
||||
size, qty = item
|
||||
expand = expand_to_structured([(size, qty)], size_unit) or []
|
||||
exceptions = [item.meta for item in expand if item.meta.get('size_text_except') or item.meta.get('qty_text_except')]
|
||||
if exceptions: print("解析异常", exceptions, '原文', sentence, entities)
|
||||
for item in expand:
|
||||
parsed_item = item.__dict__.copy()
|
||||
parsed_item['描述'] = f"{size[2]}|{qty[2]}"
|
||||
parsed_item['解析备注'] = decode_desc
|
||||
parsed_item['size_len'] = size_len
|
||||
parsed_item['qty_len'] = qty_len
|
||||
size_and_qty_parsed_result.append(parsed_item)
|
||||
|
||||
result.extend(size_and_qty_parsed_result)
|
||||
|
||||
for item in result:
|
||||
# print('item', item)
|
||||
new_item = base_ent_group_dict.copy()
|
||||
for key in mapping_size:
|
||||
col_name = mapping_size[key]
|
||||
new_item[col_name] = item[key]
|
||||
columns.append(col_name)
|
||||
size_len = item.pop('size_len', None)
|
||||
qty_len = item.pop('qty_len', None)
|
||||
calc_type = item.pop('calc_type', None)
|
||||
|
||||
if size_len > 1 and calc_type == 'total':
|
||||
new_item[f'数量'] = f"警告:异常值(总计值:{new_item[f'数量']})"
|
||||
|
||||
if size_len == 1 or size_len == qty_len:
|
||||
pass
|
||||
# new_item[f'解析备注'] = '单尺寸的订单'
|
||||
else:
|
||||
if size_len == new_item['款数']:
|
||||
new_item[f'款数'] = 1
|
||||
# new_item[f'解析备注'] = '尺寸数量和款数相同的订单'
|
||||
elif new_item['款数'] == 1:
|
||||
new_item[f'款数'] = 1
|
||||
# new_item[f'解析备注'] = '单款的订单'
|
||||
else:
|
||||
new_item[f'款数'] = "异常值:款数和尺寸数不一致"
|
||||
new_item[f'数量'] = "异常值:款数和尺寸数不一致"
|
||||
# new_item[f'解析备注'] = '多尺寸并且和款数不同的订单'
|
||||
|
||||
result_items.append(new_item)
|
||||
# print(json.dumps(new_item, indent=4, ensure_ascii=False))
|
||||
|
||||
if not result:
|
||||
if base_ent_group_dict:
|
||||
base_ent_group_dict['是否异常'] = is_except
|
||||
base_ent_group_dict[f'解析备注'] = '未解析到实际尺寸的订单1'
|
||||
result_items.append(base_ent_group_dict.copy())
|
||||
else:
|
||||
result_items.append({
|
||||
"是否异常": is_except,
|
||||
"解析备注": "未解析到任何实体"
|
||||
})
|
||||
|
||||
return exception_list, columns, result_items
|
||||
|
||||
|
||||
def parse_finance_data(file_path, target_index, is_horizontal, sheet_name="Sheet1"):
|
||||
|
||||
df = pd.read_excel(file_path, dtype=str, sheet_name=sheet_name)
|
||||
total_amount = len(df)
|
||||
|
||||
items = predict_file(df, './app/resources/models/订单尺寸识别/best_model', target_index)
|
||||
# items = predict_file(file_path, './resources/models/lintao/best_model', target_index)
|
||||
exception_list, results, outputs_columns = [], [], []
|
||||
origin_item = None
|
||||
|
||||
parsed_dict = {}
|
||||
mapping_size_values = mapping_size.values()
|
||||
|
||||
for text, origin_item, item in items:
|
||||
|
||||
decode_result = []
|
||||
|
||||
base_info = {}
|
||||
result_index = parsed_dict.get(f"{text}_index")
|
||||
if result_index:
|
||||
exceptions, columns = [], []
|
||||
try:
|
||||
decode_item = parsed_dict[text][result_index]
|
||||
for key in origin_item:
|
||||
if key in mapping_size_values:
|
||||
continue
|
||||
decode_item[key] = origin_item[key]
|
||||
decode_item['尺寸备注'] = '多组尺寸解析'
|
||||
parsed_dict[f"{text}_index"] += 1
|
||||
continue
|
||||
except IndexError:
|
||||
print(f'解析到的尺寸不足:{text},{result_index}, {parsed_dict[text]}')
|
||||
base_item = parsed_dict[text][0]
|
||||
for col in base_colume:
|
||||
base_info[col] = base_item.get(col, '')
|
||||
base_info["长"] = '异常值:没有解析到那么多尺寸'
|
||||
|
||||
decode_items = []
|
||||
parsed_dict[f"{text}_index"] += 1
|
||||
else:
|
||||
exceptions, columns, decode_items = decode([item], is_horizontal) or []
|
||||
|
||||
# if '(250731202428388557)' in text:
|
||||
# print(result_index, text, exceptions, columns, decode_items)
|
||||
# breakpoint()
|
||||
|
||||
if len(columns) > len(outputs_columns):
|
||||
outputs_columns = columns
|
||||
|
||||
if len(decode_items) > 1:
|
||||
for item in decode_items:
|
||||
item['尺寸备注'] = '多组尺寸解析'
|
||||
|
||||
for idx, decode_item in enumerate(decode_items):
|
||||
base_row = origin_item.copy()
|
||||
if idx and INDEX_COL in base_row: base_row[INDEX_COL] = ''
|
||||
# base_row = origin_item.copy() if idx == 0 else {}
|
||||
base_row.update(decode_item)
|
||||
is_except = base_row.pop('是否异常', False)
|
||||
if is_except:
|
||||
for col in base_colume:
|
||||
origin_item[col] = base_row.get(col, '')
|
||||
decode_result.append(origin_item)
|
||||
exception_list.append(base_row)
|
||||
else:
|
||||
decode_result.append(base_row)
|
||||
if not decode_items:
|
||||
if base_info:
|
||||
origin_item.update(base_info)
|
||||
|
||||
decode_result.append(origin_item)
|
||||
else:
|
||||
if is_horizontal:
|
||||
new_decode_result = decode_result[0].copy()
|
||||
for idx, item in enumerate(decode_result[1:], 1):
|
||||
for key in mapping_size_values:
|
||||
new_key = f"{key}{idx}"
|
||||
new_decode_result[new_key] = item[key]
|
||||
if new_key not in outputs_columns:
|
||||
outputs_columns.append(new_key)
|
||||
decode_result = [new_decode_result]
|
||||
|
||||
if text not in parsed_dict:
|
||||
parsed_dict[text] = decode_result
|
||||
parsed_dict[f"{text}_index"] = 0
|
||||
parsed_dict[f"{text}_index"] += 1
|
||||
|
||||
# if decode_result:
|
||||
# results.append(decode_result[0])
|
||||
if len(decode_items) > 1:
|
||||
for item in decode_result[1:]:
|
||||
item['尺寸备注'] = '复制新增:多组尺寸解析'
|
||||
results.extend(decode_result)
|
||||
|
||||
# break
|
||||
|
||||
print('横向解析', outputs_columns)
|
||||
if origin_item:
|
||||
# outputs_columns.
|
||||
_outputs_columns = list(origin_item.keys()) + outputs_columns
|
||||
col_set = set()
|
||||
outputs_columns = []
|
||||
|
||||
for col in _outputs_columns:
|
||||
if col not in col_set:
|
||||
col_set.add(col)
|
||||
outputs_columns.append(col)
|
||||
|
||||
df = pd.DataFrame(results, columns=outputs_columns)
|
||||
|
||||
file_name = os.path.basename(file_path).replace(".xlsx", '')
|
||||
filename = file_name + '_解析' + ('_横向排列' if is_horizontal else '_纵向排列')
|
||||
|
||||
upload_dir = Path("outputs")
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
save_file_path = upload_dir / f'{filename}_1.xlsx'
|
||||
|
||||
# 使用 ExcelWriter 同时写入多个 sheet
|
||||
with pd.ExcelWriter(save_file_path, engine='openpyxl') as writer:
|
||||
df.to_excel(writer, sheet_name='正常解析', index=False)
|
||||
if exception_list:
|
||||
except_df = pd.DataFrame(exception_list, columns=outputs_columns)
|
||||
except_df.to_excel(writer, sheet_name='异常解析', index=False)
|
||||
|
||||
print(f'解析结果保存在:{save_file_path}')
|
||||
return save_file_path, total_amount
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
# print(expand_to_structured([[(0,1,'600x50cm','r'), (0,1,'3款各1张','e')]]))
|
||||
# exit()
|
||||
# predict("Apple is opening a new office in Tokyo.")
|
||||
# predict("Google hired Sarah Connor from Berlin last year.")
|
||||
file_info = ['d:/会计组/新领图8月.xlsx', '备注']
|
||||
# file_info = ['d:/会计组/即客2025年8月尺寸整理.xlsx', '系统文件名']
|
||||
file_info = ['d:/会计组/8月订单明细9.6.xlsx', '文件名']
|
||||
file_info = ['d:/会计组/ZHX-8月订单明细.xlsx', '文件名']
|
||||
file_info = ['d:/会计组/国税数据源/智韬2025年8月尺寸整理(1).xlsx', '系统文件名']
|
||||
file_info = ['d:/会计组/彩印通8月数码9.23.xlsx', 'ERP系统文件名']
|
||||
file_info = ['d:/会计组/CYT8月明细9.24.xlsx', 'ERP系统文件名']
|
||||
file_info = ['d:/会计组/9.1-9.26.xlsx', '备注']
|
||||
file_info = ['d:/会计组/JD.xlsx', '文件名']
|
||||
file_info = ['d:/会计组/泰州即客2025年9月尺寸整理.xlsx', '系统文件名']
|
||||
file_info = ['d:/会计组/ZHX需拆明细9月.xlsx', '文件名', False]
|
||||
file_info = ['d:/会计组/七彩2024年9月账单尺寸整理.xlsx', '系统文件名', True]
|
||||
file_info = ['d:/会计组/艾印图文2024年9月账单尺寸整理.xlsx', '系统文件名', True]
|
||||
file_info = ['d:/会计组/9月转印.xlsx', 'erp', True]
|
||||
file_info = ['d:/会计组/9月CYT.xlsx', '文件名', False]
|
||||
file_info = ['d:/会计组/智韬2025年9月尺寸整理.xlsx', '系统文件名', True]
|
||||
file_info = ['d:/会计组/9月UV转印贴.xlsx', '文件名', True]
|
||||
file_info = ['d:/会计组/彩印通2025年9月(数码).xlsx', '文件名', True]
|
||||
file_info = ['d:/会计组/9月名片.xlsx', '文件名', True]
|
||||
file_info = ['d:/会计组/9月不干胶.xlsx', '文件名', True]
|
||||
file_path, target_index, is_horizontal = file_info
|
||||
parse_finance_data(file_path, target_index, is_horizontal)
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
pandas==2.3.3
|
||||
spacy==3.8.7
|
||||
tqdm==4.67.1
|
||||
cn2an==0.5.23
|
||||
openpyxl==3.1.5
|
||||
aiomysql
|
||||
@@ -0,0 +1,16 @@
|
||||
from typing import Optional
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.models.admin import Menu
|
||||
from app.schemas.menus import MenuCreate, MenuUpdate
|
||||
|
||||
|
||||
class MenuController(CRUDBase[Menu, MenuCreate, MenuUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Menu)
|
||||
|
||||
async def get_by_menu_path(self, path: str) -> Optional["Menu"]:
|
||||
return await self.model.filter(path=path).first()
|
||||
|
||||
|
||||
menu_controller = MenuController()
|
||||
@@ -0,0 +1,461 @@
|
||||
from tortoise.exceptions import IntegrityError
|
||||
from app.core.crud import CRUDBase
|
||||
from app.schemas.msg import (
|
||||
MsgCreate,
|
||||
MsgUpdate,
|
||||
MsgNewOrder,
|
||||
FollowFormErp,
|
||||
MsgType
|
||||
)
|
||||
import httpx
|
||||
from app.models.msg import Msg, Follow
|
||||
from app.models.weixin import WeixinCustomer, WeixinUser
|
||||
from some_sdk.wk_weixin_sdk.apis.extern_user import get_external_user_chat_info
|
||||
from some_sdk.services.binder import lintao_client, feishu_client
|
||||
from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user
|
||||
from some_sdk.feishu_sdk.apis.doc import batch_create as create_feishu_records
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from app.utils.common import async_split_generator, split_generator, gen_random_str
|
||||
from typing import List
|
||||
import re
|
||||
|
||||
import hashlib
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import asyncio
|
||||
_sync_to_feishu_lock = asyncio.Lock()
|
||||
|
||||
class MsgController(CRUDBase[Msg, MsgCreate, MsgUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Msg)
|
||||
|
||||
async def send_to_wexin(self, data:dict ):
|
||||
# pass
|
||||
url = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=57ba9cd5-2c62-43dd-bfea-78c7b4073128'
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(url,
|
||||
headers={"Content-Type": "application/json"},
|
||||
json=data,
|
||||
)
|
||||
data = resp.json()
|
||||
print(data)
|
||||
return data
|
||||
|
||||
async def send_order_to_weixin(self, msg: Msg):
|
||||
"""
|
||||
极简订单通知:仅展示店铺、旺旺ID、金额、归属客服
|
||||
"""
|
||||
url = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=57ba9cd5-2c62-43dd-bfea-78c7b4073128'
|
||||
|
||||
order_list = msg.detail or []
|
||||
if not order_list:
|
||||
logger.warning("订单详情为空,跳过发送")
|
||||
return
|
||||
|
||||
order = order_list[0]
|
||||
# users = order.get("users", [])
|
||||
# buyer = next((u for u in users if u.get("role") == "buyer"), None)
|
||||
|
||||
# 关键字段
|
||||
shop_name = order.get("shop_name", "未知店铺")
|
||||
buyer_name = msg.title
|
||||
# buyer_name = buyer.get("name") if buyer else ""
|
||||
# buyer_name = buyer_name or msg.content.split(")", 1)[0].split("(")[1]
|
||||
|
||||
is_refund = order.get("is_refund", 0) > 0
|
||||
msg_title = "💰 用户退款" if is_refund else "🛒 新订单"
|
||||
|
||||
real_payment = order.get("real_payment", "0.00")
|
||||
qiwei_name = msg.owner_name or "未知客服"
|
||||
|
||||
# 极简 Markdown
|
||||
markdown_content = f"""# {msg_title}\n
|
||||
|
||||
**店铺**:{shop_name}
|
||||
**旺旺ID**:`{buyer_name}`
|
||||
**金额**:¥{real_payment}
|
||||
**企微客服**:{qiwei_name}
|
||||
**订单编号**:{order.get("trade_no", "未知订单号")}
|
||||
"""
|
||||
|
||||
data = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"content": markdown_content
|
||||
}
|
||||
}
|
||||
result = await self.send_to_wexin(data)
|
||||
logger.info(f"企业微信极简订单通知发送结果: {result}")
|
||||
|
||||
|
||||
async def parse_order_msg(self, msg: Msg, send_set: set, msg_dict: dict):
|
||||
title = msg.title
|
||||
try:
|
||||
if title not in send_set:
|
||||
send_set.add(title)
|
||||
# try: await self.send_to_wexin(msg)
|
||||
owner_name = '、'.join([ (name or '').split('(')[0].split('vip客服-')[-1].split('-印刷定制')[0] for name in msg_dict[title] ])msg.owner_name = owner_name
|
||||
|
||||
try: await self.send_order_to_weixin(msg)
|
||||
except Exception as e:
|
||||
logger.error(f'发送订单消息到微信失败,{e}')
|
||||
return
|
||||
|
||||
# await self.send_to_wexin(msg, mentioned_list=msg_dict[title])
|
||||
logger.info(f'发送消息到微信成功,{msg.content}')
|
||||
|
||||
msg.is_send = True
|
||||
msg.send_at = datetime.now()
|
||||
await msg.save()
|
||||
except Exception as e:
|
||||
logger.error(f'发送消息到微信失败,{e}')
|
||||
|
||||
async def new_system_msg(self, title: str, content: str, owner_name: str = None):
|
||||
await self.create(
|
||||
dict(
|
||||
hash_id=hashlib.md5(f'{title}{content}{owner_name}'.encode()).hexdigest(),
|
||||
title=title,
|
||||
content=content,
|
||||
type=MsgType.SYSTEM,
|
||||
owner_name=owner_name,
|
||||
)
|
||||
)
|
||||
|
||||
async def send_system_msg(self, msg: Msg):
|
||||
msg.send_at = datetime.now()
|
||||
markdown_content = f"""# 📣 系统通知 \n
|
||||
## {msg.title} \n
|
||||
{msg.content} \n
|
||||
|
||||
{msg.send_at.strftime("%Y-%m-%d %H:%M:%S")}
|
||||
"""
|
||||
|
||||
data = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"content": markdown_content
|
||||
}
|
||||
}
|
||||
result = await self.send_to_wexin(data)
|
||||
logger.info(f"企业微信极简订单通知发送结果: {result}")
|
||||
|
||||
msg.is_send = True
|
||||
await msg.save()
|
||||
|
||||
async def sync_msg(self):
|
||||
msg_list = await self.model.filter(is_send=False)
|
||||
|
||||
msg_dict = {}
|
||||
for msg in msg_list:
|
||||
title = msg.title
|
||||
msg_dict[title] = msg_dict.get(title, []) + [msg.owner_name]
|
||||
|
||||
send_set = set()
|
||||
for msg in msg_list:
|
||||
title = msg.title
|
||||
if msg.type == MsgType.NEW_ORDER:
|
||||
await self.parse_order_msg(msg, send_set, msg_dict)
|
||||
elif msg.type == MsgType.SYSTEM:
|
||||
await self.send_system_msg(msg)
|
||||
|
||||
async def fill_customer_info(self):
|
||||
msg_orm_list = await self.model.filter(id__gt=306)
|
||||
|
||||
update_list = []
|
||||
for msg in msg_orm_list:
|
||||
detail = msg.detail or []
|
||||
taobao_name = msg.title
|
||||
|
||||
detail_list = []
|
||||
for order_item in detail:
|
||||
customer_orm = await WeixinCustomer.filter(taobao_name=taobao_name).first()
|
||||
if not customer_orm:
|
||||
logger.warning(f'未找到淘宝用户{taobao_name}的企微客户')
|
||||
continue
|
||||
|
||||
for user in order_item.get("users", []):
|
||||
if user.get("role") != "buyer": continue
|
||||
user.update(customer_orm.to_dict())
|
||||
break
|
||||
|
||||
detail_list.append(order_item)
|
||||
|
||||
if detail_list:
|
||||
msg.detail = detail_list
|
||||
update_list.append(msg)
|
||||
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, fields=['detail'])
|
||||
|
||||
|
||||
async def new_order(self, order_id: str, customer_orm: WeixinCustomer, is_refund: bool = False):
|
||||
|
||||
msg_set = set()
|
||||
msg_list = []
|
||||
now = datetime.now()
|
||||
|
||||
order_list = get_order_relative_user(lintao_client, order_id)
|
||||
order_unique_list = {}
|
||||
async for order in order_list:
|
||||
order_state_string = order.get('order_state_string')
|
||||
trade_no = order.get('trade_no')
|
||||
if not is_refund and order_state_string not in ['待领单', '待抢单']:
|
||||
logger.warning(f'订单{trade_no} 状态为{order_state_string},跳过')
|
||||
continue
|
||||
|
||||
remark = order.get('remark')
|
||||
if not is_refund and remark and '企微联系' in remark:
|
||||
logger.warning(f'订单{trade_no} 状态为{order_state_string}, 备注:{remark}, 已备注企微联系,跳过')
|
||||
continue
|
||||
|
||||
create_time = order.get('create_time')
|
||||
if create_time and not is_refund:
|
||||
create_time = datetime.fromisoformat(create_time.replace("Z", "+00:00"))
|
||||
if (now - create_time).total_seconds() > 3600:
|
||||
logger.warning(f'订单{order_id} 创建时间({create_time})与当前时间({now})相差超过60秒,跳过')
|
||||
continue
|
||||
|
||||
if trade_no not in order_unique_list:
|
||||
order_unique_list[trade_no] = order
|
||||
logger.info(f'订单{order_id} 订单号{trade_no} 创建时间{create_time} 订单状态:{order_state_string} {order.get("title")}')
|
||||
|
||||
# 填充买家信息
|
||||
for user in order.get("users", []):
|
||||
if user.get("role") != "buyer": continue
|
||||
user.update(customer_orm.to_dict())
|
||||
break
|
||||
|
||||
|
||||
if not order_unique_list:
|
||||
logger.warning(f'订单{order_id} 没有符合条件的订单,跳过')
|
||||
return
|
||||
|
||||
chat_info = await get_external_user_chat_info(customer_orm.weixin_id)
|
||||
follow_user_list = chat_info.get('follow_user', [])
|
||||
|
||||
order_list = list(order_unique_list.values())
|
||||
order_info = [
|
||||
f"在({order.get('shop_name')})下单{order.get('price')}元,单号({order.get('trade_no')})"
|
||||
for order in order_list
|
||||
]
|
||||
for follow_user in follow_user_list:
|
||||
follow_userid = follow_user.get('userid')
|
||||
weixin_user_orm: WeixinUser = await WeixinUser.filter(userid=follow_userid).first()
|
||||
owner_name = '未知'
|
||||
if weixin_user_orm: owner_name = weixin_user_orm.english_name or weixin_user_orm.username
|
||||
|
||||
qiwei_customer_name = customer_orm.taobao_name or customer_orm.weixin_name
|
||||
|
||||
content = f"{'退款' if is_refund else ''}订单({qiwei_customer_name}) -({owner_name})- 刚{', '.join(order_info)}"
|
||||
logger.info(f'新订单通知:{content}')
|
||||
msg_hash_id = hashlib.md5(content.encode('utf-8')).hexdigest()
|
||||
if msg_hash_id in msg_set: continue
|
||||
msg_set.add(msg_hash_id)
|
||||
|
||||
msg = self.model(
|
||||
hash_id=msg_hash_id,
|
||||
title=qiwei_customer_name,
|
||||
content=content,
|
||||
detail=order_list,
|
||||
type=MsgType.NEW_ORDER,
|
||||
owner_id=follow_userid,
|
||||
owner_name=owner_name
|
||||
)
|
||||
msg_list.append(msg)
|
||||
|
||||
if msg_list:
|
||||
try: await self.model.bulk_create(msg_list)
|
||||
except IntegrityError as e:
|
||||
if "Duplicate entry" in str(e) and "hash_id" in str(e):
|
||||
logger.info("检测到重复消息,已跳过")
|
||||
# 忽略或处理重复
|
||||
else:
|
||||
# 其他完整性错误(如外键失败),应重新抛出
|
||||
raise
|
||||
|
||||
async def get_order_user_days_before(self, days: int):
|
||||
now = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
start_date, otherMemo = (now - timedelta(days=days)).strftime("%Y-%m-%d %H:%M:%S"), "企微"
|
||||
|
||||
# 琪琪——松屿,小小丽——麦子,小小北——大喜,小泉——苏苏,小洋——东东,蓝莓——小满
|
||||
index = 0
|
||||
update_list, add_list = [], []
|
||||
order_iter_list = [
|
||||
(get_order_relative_user(lintao_client, start_date=start_date, otherMemo=otherMemo), ''),
|
||||
(get_order_relative_user(lintao_client, start_date=start_date, customer='琪琪'), '松屿'),
|
||||
(get_order_relative_user(lintao_client, start_date=start_date, customer='小小丽'), '麦子'),
|
||||
(get_order_relative_user(lintao_client, start_date=start_date, customer='小小北'), '大喜'),
|
||||
(get_order_relative_user(lintao_client, start_date=start_date, customer='小泉'), '苏苏'),
|
||||
(get_order_relative_user(lintao_client, start_date=start_date, customer='小洋'), '东东'),
|
||||
(get_order_relative_user(lintao_client, start_date=start_date, customer='蓝莓'), '小满'),
|
||||
]
|
||||
for order_iter, staff_name in order_iter_list:
|
||||
async for order_list in async_split_generator(order_iter, 10):
|
||||
order_dict = {}
|
||||
for order in order_list:
|
||||
designer_list = list(filter(lambda x: x.get("role") == "desiger", order.get("users", [])))
|
||||
designer = designer_list[0] if designer_list else None
|
||||
if not designer: continue
|
||||
|
||||
name = designer.get("name", "")
|
||||
if not name: continue
|
||||
|
||||
key = tuple([order.get("trade_no", ""), name])
|
||||
if key in order_dict: continue
|
||||
order_dict[key] = order
|
||||
index += 1
|
||||
|
||||
_update_list, _add_list = await self.process_order_dict(order_dict, staff_name=staff_name)
|
||||
|
||||
if _update_list and _add_list:
|
||||
logger.info(f'第{index}个订单 处理 {len(_update_list)} 条更新记录, {len(_add_list)} 条新增记录')
|
||||
update_list.extend(_update_list)
|
||||
add_list.extend(_add_list)
|
||||
|
||||
print(start_date)
|
||||
_update_list, _add_list = [], []
|
||||
|
||||
async with _sync_to_feishu_lock:
|
||||
task_id = gen_random_str()
|
||||
_update_list, _add_list = await self.sync_to_feishu(task_id)
|
||||
|
||||
return {
|
||||
"update_count": len(update_list),
|
||||
"add_count": len(add_list),
|
||||
"add_count_to_feishu": len(_add_list),
|
||||
"update_count_to_feishu": len(_update_list),
|
||||
"start_date": start_date,
|
||||
"remark": otherMemo
|
||||
}
|
||||
|
||||
async def process_order_dict(self, order_dict: dict, staff_name: str = ''):
|
||||
async def to_follow_orm(order_info: dict):
|
||||
follow_orm = Follow(**FollowFormErp(**order_info).model_dump())
|
||||
# print(follow_orm.order_id, order_info.get("users", []))
|
||||
|
||||
for user in order_info.get("users", []):
|
||||
if user.get("role") == "desiger":
|
||||
follow_orm.designer_id = user.get("id", "")
|
||||
follow_orm.designer_name = user.get("name", "")
|
||||
elif user.get("role") == "taobao_kefu":
|
||||
follow_orm.kefu_id = user.get("id", "")
|
||||
follow_orm.kefu_name = user.get("name", "")
|
||||
elif user.get("role") == "buyer":
|
||||
follow_orm.customer_taobao_id = user.get("id", "")
|
||||
follow_orm.customer_name = user.get("name", "")
|
||||
|
||||
CustomerUser = await WeixinCustomer.filter(taobao_id=follow_orm.customer_taobao_id).first()
|
||||
if CustomerUser:
|
||||
follow_orm.customer_id = CustomerUser.id
|
||||
|
||||
# 企微员工
|
||||
if staff_name:
|
||||
follow_orm.staff_name = staff_name
|
||||
staff_orm = await WeixinUser.filter(english_name=f"{follow_orm.staff_name}-印刷定制").first()
|
||||
if staff_orm:
|
||||
follow_orm.staff_id = staff_orm.userid
|
||||
else:
|
||||
remark = follow_orm.remark
|
||||
if remark:
|
||||
staff_name_match = re.search(r"企微联系(.*?)拉群", remark)
|
||||
if staff_name_match:
|
||||
follow_orm.staff_name = staff_name_match.group(1).strip()
|
||||
if follow_orm.staff_name:
|
||||
staff_orm = await WeixinUser.filter(english_name=f"{follow_orm.staff_name}-印刷定制").first()
|
||||
if staff_orm:
|
||||
follow_orm.staff_id = staff_orm.userid
|
||||
|
||||
if not staff_name_match:
|
||||
follow_orm.staff_name = remark
|
||||
|
||||
return follow_orm
|
||||
|
||||
update_list = []
|
||||
follow_list = await Follow.filter(order_id__in=[key[0] for key in order_dict.keys()])
|
||||
keys, update_keys = Follow.get_all_keys(exclude_fields=['id', 'updated_at', 'created_at', 'feishu_record_id', 'is_update_to_feishu', 'is_add_to_feishu']), set()
|
||||
for follow in follow_list:
|
||||
key = tuple([follow.order_id, follow.designer_name])
|
||||
if key not in order_dict: continue
|
||||
|
||||
order_info = order_dict.pop(key, None)
|
||||
follow_orm = await to_follow_orm(order_info)
|
||||
# 比较是否一样,不一样的话,更新
|
||||
updated_dict = {}
|
||||
for key in keys:
|
||||
origin_value = str(getattr(follow, key))
|
||||
value = str(getattr(follow_orm, key))
|
||||
if origin_value != value:
|
||||
update_keys.add(key)
|
||||
setattr(follow, key, value)
|
||||
updated_dict[key] = f'{origin_value} -> {value}'
|
||||
|
||||
if updated_dict:
|
||||
follow_orm.is_update_to_feishu = False
|
||||
update_keys.add('is_update_to_feishu')
|
||||
update_list.append(follow)
|
||||
logger.info(f'订单 {follow.order_id} 设计员 {follow.designer_name} 更新字段 {updated_dict}')
|
||||
|
||||
if update_list:
|
||||
await Follow.bulk_update(update_list, fields=update_keys)
|
||||
|
||||
add_list = []
|
||||
for key, order_info in order_dict.items():
|
||||
follow_orm = await to_follow_orm(order_info)
|
||||
add_list.append(follow_orm)
|
||||
|
||||
if add_list:
|
||||
await Follow.bulk_create(add_list)
|
||||
|
||||
return update_list, add_list
|
||||
|
||||
async def sync_to_feishu(self, task_id: str):
|
||||
|
||||
add_list = []
|
||||
# 需要追加的记录
|
||||
follow_list = await Follow.filter(is_update_to_feishu=False, feishu_record_id__isnull=True)
|
||||
if follow_list:
|
||||
for item_list in split_generator(follow_list, 100):
|
||||
result = await create_feishu_records(
|
||||
feishu_client,
|
||||
app_token="HPiNbbW3YaYhBjsjLQychK9gnbf",
|
||||
table_id="tblQGVfmglJgZuBf",
|
||||
records=[{"fields": item.to_feishu_record(fields={"同步任务号": task_id})["fields"]} for item in item_list],
|
||||
)
|
||||
|
||||
_update_list = []
|
||||
result_list = result.get("data", {}).get("records", [])
|
||||
for record, item in zip(result_list, item_list):
|
||||
item.feishu_record_id = record["record_id"]
|
||||
item.is_add_to_feishu = True
|
||||
item.is_update_to_feishu = True
|
||||
_update_list.append(item)
|
||||
|
||||
if _update_list:
|
||||
await Follow.bulk_update(_update_list, fields=['feishu_record_id', 'is_add_to_feishu', 'is_update_to_feishu'])
|
||||
|
||||
logger.info(f'追加 {len(_update_list)} 条记录到飞书')
|
||||
add_list.extend(_update_list)
|
||||
|
||||
update_list = []
|
||||
# 需要更新的记录
|
||||
# follow_list = await Follow.filter(is_update_to_feishu=False, feishu_record_id__isnull=False)
|
||||
# if follow_list:
|
||||
# for item_list in split_generator(follow_list, 100):
|
||||
# await create_feishu_records(
|
||||
# get_feishu_client,
|
||||
# app_token="JTqnbdjb7aEMm3srrLfcidYcnxc",
|
||||
# table_id="tblXq8AmG22uK5n1",
|
||||
# records=item_list,
|
||||
# )
|
||||
|
||||
return update_list, add_list
|
||||
|
||||
async def set_read(self, id: str):
|
||||
await Msg.filter(id=id).update(is_read=True, read_at=datetime.now())
|
||||
|
||||
msg_controller = MsgController()
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from typing import List
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.models.admin import Api, Menu, Role
|
||||
from app.schemas.roles import RoleCreate, RoleUpdate
|
||||
|
||||
|
||||
class RoleController(CRUDBase[Role, RoleCreate, RoleUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Role)
|
||||
|
||||
async def is_exist(self, name: str) -> bool:
|
||||
return await self.model.filter(name=name).exists()
|
||||
|
||||
async def update_roles(self, role: Role, menu_ids: List[int], api_infos: List[dict]) -> None:
|
||||
await role.menus.clear()
|
||||
for menu_id in menu_ids:
|
||||
menu_obj = await Menu.filter(id=menu_id).first()
|
||||
await role.menus.add(menu_obj)
|
||||
|
||||
await role.apis.clear()
|
||||
for item in api_infos:
|
||||
api_obj = await Api.filter(path=item.get("path"), method=item.get("method")).first()
|
||||
await role.apis.add(api_obj)
|
||||
|
||||
|
||||
role_controller = RoleController()
|
||||
@@ -0,0 +1,62 @@
|
||||
from typing import List
|
||||
|
||||
import json
|
||||
from app.core.crud import CRUDBase
|
||||
from app.models.admin import Api, Menu, Codegen
|
||||
from app.schemas.codegen import CodegenCreate, CodegenUpdate
|
||||
from app.utils.db import DatabaseInfo
|
||||
from app.utils.codegen import CodeGenerator
|
||||
|
||||
class CodegenController(CRUDBase[Codegen, CodegenCreate, CodegenUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Codegen)
|
||||
|
||||
async def is_exist(self, name: str) -> bool:
|
||||
return await self.model.filter(name=name).exists()
|
||||
|
||||
async def import_table(self, connect: str='', importTables: List[str] = None) -> Codegen:
|
||||
|
||||
default_frontend_config = {
|
||||
"editable": True,
|
||||
"listable": True,
|
||||
"sortable": True,
|
||||
"filterable": False,
|
||||
"filter_operator": '=', # = | >= 默认过滤操作符为包含
|
||||
"display_type": 'text', # 默认显示类型为文本
|
||||
}
|
||||
|
||||
cnt = 0
|
||||
|
||||
with DatabaseInfo(
|
||||
host="lt.330770.xyz",
|
||||
port=3307,
|
||||
user="root",
|
||||
password="rap_sky",
|
||||
database="rpa"
|
||||
) as db:
|
||||
|
||||
for table_name in importTables:
|
||||
table = db.get_table_structure(table_name)
|
||||
# result.append(fields)
|
||||
fields = table.get('fields')
|
||||
for field in fields:
|
||||
# if field.name in ['id']:
|
||||
if 'editable' not in field:
|
||||
field['editable'] = field['required']
|
||||
new_data = {**default_frontend_config, **field}
|
||||
field.update(**new_data)
|
||||
description = table.get('tableComment')
|
||||
await self.create(CodegenCreate(name=table_name, description=description , fields=json.dumps(fields,ensure_ascii=False)))
|
||||
cnt += 1
|
||||
|
||||
return cnt
|
||||
|
||||
async def preview(self, table_id):
|
||||
generator = CodeGenerator()
|
||||
config_orm = await self.get(id=table_id)
|
||||
entity_config = await config_orm.to_dict()
|
||||
generated_files = generator.generate_files(entity_config, 'relation-demo', {})
|
||||
return generated_files
|
||||
|
||||
|
||||
codegen_controller = CodegenController()
|
||||
@@ -0,0 +1,60 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi.exceptions import HTTPException
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.models.admin import User
|
||||
from app.schemas.login import CredentialsSchema
|
||||
from app.schemas.users import UserCreate, UserUpdate
|
||||
from app.utils.password import get_password_hash, verify_password
|
||||
|
||||
from .role import role_controller
|
||||
|
||||
|
||||
class UserController(CRUDBase[User, UserCreate, UserUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=User)
|
||||
|
||||
async def get_by_email(self, email: str) -> Optional[User]:
|
||||
return await self.model.filter(email=email).first()
|
||||
|
||||
async def get_by_username(self, username: str) -> Optional[User]:
|
||||
return await self.model.filter(username=username).first()
|
||||
|
||||
async def create_user(self, obj_in: UserCreate) -> User:
|
||||
obj_in.password = get_password_hash(password=obj_in.password)
|
||||
obj = await self.create(obj_in)
|
||||
return obj
|
||||
|
||||
async def update_last_login(self, id: int) -> None:
|
||||
user = await self.model.get(id=id)
|
||||
user.last_login = datetime.now()
|
||||
await user.save()
|
||||
|
||||
async def authenticate(self, credentials: CredentialsSchema) -> Optional["User"]:
|
||||
user = await self.model.filter(username=credentials.username).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="无效的用户名")
|
||||
verified = verify_password(credentials.password, user.password)
|
||||
if not verified:
|
||||
raise HTTPException(status_code=400, detail="密码错误!")
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=400, detail="用户已被禁用")
|
||||
return user
|
||||
|
||||
async def update_roles(self, user: User, role_ids: List[int]) -> None:
|
||||
await user.roles.clear()
|
||||
for role_id in role_ids:
|
||||
role_obj = await role_controller.get(id=role_id)
|
||||
await user.roles.add(role_obj)
|
||||
|
||||
async def reset_password(self, user_id: int):
|
||||
user_obj = await self.get(id=user_id)
|
||||
if user_obj.is_superuser:
|
||||
raise HTTPException(status_code=403, detail="不允许重置超级管理员密码")
|
||||
user_obj.password = get_password_hash(password="123456")
|
||||
await user_obj.save()
|
||||
|
||||
|
||||
user_controller = UserController()
|
||||
@@ -0,0 +1,607 @@
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.schemas.weixin import (
|
||||
WeixinCustomerCreate,
|
||||
WeixinCustomerUpdate,
|
||||
WeixinCustomerXingyunCreate,
|
||||
)
|
||||
from typing import List
|
||||
|
||||
from app.models.weixin import WeixinCustomer, WeixinUser, CustomerGroup
|
||||
from app.schemas.weixin import WeixinUserBindInfo, WeixinOrderBindInfo
|
||||
|
||||
from some_sdk.services.binder import lintao_client, xy_client, wk_client
|
||||
from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user, save_other_memo
|
||||
from some_sdk.xingyun_sdk.apis.customer import bind_user_to_xingyun, list_user_order
|
||||
from some_sdk.xingyun_sdk.apis.work_external_contact import list_all_contact_by_addtime, list_all_contact_by_addtime_and_tag
|
||||
from some_sdk.wk_weixin_sdk.apis.corp_user import from_service_external_userid
|
||||
from some_sdk.wk_weixin_sdk.apis.extern_user import get_external_user_chat_info
|
||||
|
||||
from app.controllers.msg import msg_controller
|
||||
from app.schemas.msg import MsgType
|
||||
|
||||
from app.utils.common import async_split_generator
|
||||
import pickle
|
||||
import hashlib
|
||||
|
||||
import re, os
|
||||
from datetime import datetime, timedelta
|
||||
from app.core.cache import cache_if
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EXCLUDE_SHOP_NAMES = ['领淘文具旗舰店']
|
||||
XINGYUN_USE_TIME = datetime(2025, 6, 18)
|
||||
|
||||
class WeixinCustomerController(CRUDBase[WeixinCustomer, WeixinCustomerCreate, WeixinCustomerUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=WeixinCustomer)
|
||||
|
||||
async def update_from_group(self, external_members: list[dict]):
|
||||
# 同步更新数据库
|
||||
member_dict = {}
|
||||
for member in external_members:
|
||||
member = member.copy()
|
||||
userid = member.pop('userid', None)
|
||||
|
||||
weixin_name = member.pop('name', None)
|
||||
weixin_unionid = member.pop('unionid', None)
|
||||
|
||||
member_info = WeixinCustomer.orm_format(member)
|
||||
member_info['weixin_id'] = userid
|
||||
member_info['weixin_name'] = weixin_name
|
||||
member_info['weixin_unionid'] = weixin_unionid
|
||||
member_dict[userid] = member_info
|
||||
|
||||
member_list = await self.model.filter(weixin_id__in=member_dict.keys())
|
||||
|
||||
update_list = []
|
||||
update_fileds = ['weixin_unionid', 'weixin_id', 'weixin_name', 'order_id', 'taobao_id', 'taobao_name', 'extra', 'need_confirm']
|
||||
|
||||
history = {}
|
||||
for member in member_list:
|
||||
member_info = member_dict.pop(member.weixin_id, history.get(member.weixin_id, {}))
|
||||
if not member_info: continue
|
||||
history[member.weixin_id] = member_info
|
||||
|
||||
member_info['extra'] = {**(member.extra or {}), **member_info['extra']}
|
||||
|
||||
updated = {}
|
||||
for field in update_fileds:
|
||||
old_value = getattr(member, field)
|
||||
if field not in member_info:
|
||||
continue
|
||||
value = member_info.get(field)
|
||||
if old_value != value:
|
||||
setattr(member, field, value)
|
||||
updated[f'{field}:{str(old_value)}'] = value
|
||||
if updated:
|
||||
logger.info(f'用户 {member.weixin_name} 更新关系为 {updated}')
|
||||
update_list.append(member)
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, fields=update_fileds)
|
||||
|
||||
# 新增用户
|
||||
create_list = []
|
||||
for userid, member_info in member_dict.items():
|
||||
create_list.append(WeixinCustomer(
|
||||
**member_info,
|
||||
))
|
||||
if create_list:
|
||||
await self.model.bulk_create(create_list)
|
||||
|
||||
return {
|
||||
'update_count': len(update_list),
|
||||
'create_count': len(create_list),
|
||||
}
|
||||
|
||||
async def bind_user(self, user_in: WeixinUserBindInfo):
|
||||
if not user_in.taobao_id:
|
||||
raise ValueError("淘宝ID不能为空")
|
||||
await self.model.filter(taobao_id=user_in.taobao_id).update_from_dict(user_in.model_dump(exclude_unset=True))
|
||||
|
||||
async def remark_order(self, order: dict, remark: str):
|
||||
ctid = order.get("ctid") or order.get("trade_no")
|
||||
# if remark in (order.get("remark") or ''):
|
||||
if '企微' in (order.get("remark") or ''):
|
||||
logger.info(f'订单 {ctid} 备注中已包含 企微 备注,无需重复添加')
|
||||
return
|
||||
remark_list = [order.get("remark", ""), remark]
|
||||
append_remark = ';'.join(filter(lambda x: x, remark_list))
|
||||
|
||||
if os.getenv("APP_ENV") == "prod":
|
||||
result = await save_other_memo(lintao_client, order_id=ctid, other_memo=append_remark)
|
||||
assert result["type"] == "success", f"保存备注失败,响应: {result}"
|
||||
logger.info(f'成功为订单 {ctid} 打上备注 {append_remark}')
|
||||
else:
|
||||
logger.info(f'非生产环境,不实际为订单 {ctid} 打上备注 {append_remark}')
|
||||
|
||||
async def remark_order_by_order_id(self, remark: str, order_id: str=None, orders: list=None):
|
||||
if not orders:
|
||||
assert order_id, "订单ID不能为空"
|
||||
orders = [o async for o in get_order_relative_user(lintao_client, trade_no=order_id)]
|
||||
assert orders, f"订单 {order_id} 不存在"
|
||||
|
||||
# 打上备注
|
||||
logger.info(f'即将为以下订单 {[o.get("trade_no") for o in orders]} 打上备注 {remark}')
|
||||
need_monitor_order_list = []
|
||||
for order in orders:
|
||||
if not order.get('title'):
|
||||
# 未领单的订单,需要添加到监控列表中进行监控,防止被刷掉
|
||||
need_monitor_order_list.append(order)
|
||||
logger.info(f'订单 {order.get("trade_no")} 未领单,需要添加到监控列表中进行监控,防止被刷掉')
|
||||
|
||||
try:
|
||||
await self.remark_order(order, remark=remark)
|
||||
except Exception as e:
|
||||
logger.error(f'为订单 {order.get("trade_no")} 打上备注 {remark} 失败,异常: {e}')
|
||||
need_monitor_order_list.append(order)
|
||||
|
||||
return need_monitor_order_list
|
||||
|
||||
|
||||
async def bind_order(self, bind_in: WeixinOrderBindInfo):
|
||||
orders = [o async for o in get_order_relative_user(lintao_client, trade_no=bind_in.order_id)]
|
||||
assert orders, f"订单 {bind_in.order_id} 不存在"
|
||||
|
||||
orders = [o for o in orders if o.get('trade_no') == bind_in.order_id]
|
||||
order = orders[0]
|
||||
|
||||
buyer_ids = []
|
||||
for user in order.get('users', []):
|
||||
taobao_id = user.get('id', '')
|
||||
if not taobao_id or re.match(r'^\d+$', taobao_id): continue
|
||||
buyer_ids.append(taobao_id)
|
||||
|
||||
# 更新还是新建?
|
||||
orms = await self.model.filter(weixin_id=bind_in.userid).all()
|
||||
logger.info(f'用户 {bind_in.userid} 准备绑定订单 {len(orms)} {[orm.weixin_name for orm in orms]}')
|
||||
|
||||
update_list = []
|
||||
for orm in orms:
|
||||
if orm.order_id and orm.order_id != order.get('trade_no'):
|
||||
logger.warning(f'用户 {orm.weixin_name} 已绑定订单 {orm.order_id},即将覆盖为 {order.get("trade_no")}')
|
||||
|
||||
orm.weixin_id = bind_in.userid
|
||||
orm.order_id = order.get('trade_no')
|
||||
orm.shop_name = order.get('shop_name', '')
|
||||
orm.taobao_id = user.get('id', '')
|
||||
orm.taobao_name = user.get('name', '')
|
||||
update_list.append(orm)
|
||||
|
||||
if update_list:
|
||||
logger.info(f'更新用户 {bind_in.userid} 绑定订单 {order.get("trade_no")} 中的用户 {user}')
|
||||
await self.model.bulk_update(update_list, fields=['order_id', 'weixin_id', 'taobao_id', 'taobao_name'])
|
||||
return True, orders
|
||||
|
||||
if not orms:
|
||||
logger.info(f'绑定用户 {bind_in.userid} 绑定订单 {order.get("trade_no")} 中的用户 {user}')
|
||||
orm_user = WeixinCustomer(
|
||||
weixin_id=bind_in.userid,
|
||||
order_id=order.get('trade_no'),
|
||||
taobao_id=user.get('id', ''),
|
||||
taobao_name=user.get('name', ''),
|
||||
)
|
||||
await orm_user.save()
|
||||
return True, orders
|
||||
|
||||
return False, orders
|
||||
|
||||
async def bind_xingyun_user(self, contact_orm: WeixinCustomer):
|
||||
assert contact_orm.xingyun_id, "星云用户ID不能为空"
|
||||
if contact_orm.shop_name in EXCLUDE_SHOP_NAMES:
|
||||
contact_orm.xingyun_sync = True
|
||||
contact_orm.extra = contact_orm.extra or {}
|
||||
contact_orm.extra['system_remark'] = f'店铺名称: {contact_orm.shop_name},没有开通星云有客服务,故此跳过。'
|
||||
return
|
||||
|
||||
order_id = contact_orm.order_id.split('_')[-1]
|
||||
logger.info(f'同步客户订单信息到星云: {contact_orm.xingyun_id} -》 {order_id}')
|
||||
try:
|
||||
await bind_user_to_xingyun(xy_client, contact_orm.xingyun_id, order_id)
|
||||
except:
|
||||
logger.error(f'同步客户订单信息到星云失败: {contact_orm.xingyun_id} -》 {order_id}, 尝试寻找同用户的其他订单来绑定')
|
||||
shop_name = ''
|
||||
async for order in get_order_relative_user(lintao_client, buyer_nick=contact_orm.taobao_name):
|
||||
shop_name = order.get('shop_name', '')
|
||||
if shop_name in EXCLUDE_SHOP_NAMES:
|
||||
continue
|
||||
|
||||
if not order.get('create_time'): continue
|
||||
create_time = order.get('create_time')
|
||||
create_time = datetime.fromisoformat(create_time.replace("Z", "+00:00"))
|
||||
if create_time < XINGYUN_USE_TIME:
|
||||
continue
|
||||
|
||||
order_id = order.get('trade_no').split('_')[-1]
|
||||
await bind_user_to_xingyun(xy_client, contact_orm.xingyun_id, order_id)
|
||||
logger.info(f'通过同用户的其他订单来同步客户订单信息到星云成功: {contact_orm.xingyun_id} -》 {order_id}')
|
||||
break
|
||||
|
||||
if shop_name and not contact_orm.shop_name:
|
||||
contact_orm.shop_name = order.get('shop_name', '')
|
||||
await contact_orm.save()
|
||||
|
||||
contact_orm.xingyun_sync = True
|
||||
|
||||
async def load_all_user_from_xingyun(self, task_id: str, add_time_start: datetime, add_time_end: datetime):
|
||||
logger.info(f'从星云加载客户数据,时间范围 {task_id} {add_time_start} - {add_time_end}')
|
||||
contact_iter = list_all_contact_by_addtime_and_tag(xy_client, task_id=task_id, keyword='', add_time_start=add_time_start, add_time_end=add_time_end)
|
||||
|
||||
return await self._load_user_from_xingyun(contact_iter, eager_load=True)
|
||||
|
||||
async def load_user_from_xingyun(self, add_time_start: datetime, add_time_end: datetime):
|
||||
logger.info(f'从星云加载客户数据,时间范围 {add_time_start} - {add_time_end}')
|
||||
contact_iter = list_all_contact_by_addtime(xy_client, keyword='', add_time_start=add_time_start, add_time_end=add_time_end)
|
||||
|
||||
return await self._load_user_from_xingyun(contact_iter)
|
||||
|
||||
async def _load_user_from_xingyun(self, contact_iter: list, eager_load: bool = False):
|
||||
logger.info(f'从星云加载客户数据,开始同步')
|
||||
load_result = {"update_count": 0, "add_count": 0}
|
||||
async for contact_list in async_split_generator(contact_iter, 10):
|
||||
try: result = await self.save_xingyun_contact_info(contact_list)
|
||||
except Exception as e:
|
||||
logger.error(f'从星云加载客户数据,同步失败 {e}')
|
||||
logger.exception(e)
|
||||
raise e
|
||||
|
||||
# 全部都已经同步完了
|
||||
if not eager_load and result.get("update_count", 0) + result.get("add_count", 0) == 0:
|
||||
logger.info(f'从星云加载客户数据,全部同步完成')
|
||||
break
|
||||
|
||||
load_result['update_count'] += result.get("update_count", 0)
|
||||
load_result['add_count'] += result.get("add_count", 0)
|
||||
logger.info(f'从星云加载客户数据{eager_load},更新 {result.get("update_count", 0)} 条,新增 {result.get("add_count", 0)} 条')
|
||||
logger.info(f'从星云加载客户数据,总 {load_result["update_count"] + load_result["add_count"]} 条,更新 {load_result["update_count"]} 条,新增 {load_result["add_count"]} 条')
|
||||
await self.sync_xingyun_contact_info()
|
||||
|
||||
return load_result
|
||||
|
||||
async def save_xingyun_contact_info(self, contact_list: list):
|
||||
contact_dict = {contact.get('cid'): contact for contact in contact_list}
|
||||
contact_orm_list: List[WeixinCustomer] = await self.model.filter(xingyun_id__in=contact_dict.keys())
|
||||
|
||||
field_names = set()
|
||||
update_list = []
|
||||
|
||||
async def update(contact_dict, contact_orm_list, key):
|
||||
history = {}
|
||||
for contact_orm in contact_orm_list:
|
||||
value = getattr(contact_orm, key)
|
||||
contact = contact_dict.pop(value, history.get(value, {}))
|
||||
if not contact: continue
|
||||
history[value] = contact
|
||||
|
||||
try:
|
||||
contact_in = WeixinCustomerXingyunCreate(**contact).transform_to_extra()
|
||||
except Exception as e:
|
||||
logger.error(f'客户数据转换错误,chat_id: {contact}, {e}')
|
||||
continue
|
||||
# print(type(contact_in), contact_in)
|
||||
updated = False
|
||||
|
||||
contact_in_dict = contact_in.model_dump(exclude_unset=True)
|
||||
field_names.update(contact_in_dict.keys())
|
||||
|
||||
for field_name in field_names:
|
||||
|
||||
old_value = getattr(contact_orm, field_name)
|
||||
value = contact_in_dict.get(field_name)
|
||||
if old_value != value:
|
||||
setattr(contact_orm, field_name, value)
|
||||
updated = True
|
||||
|
||||
new_extra = {**(contact_orm.extra or {}), **contact_in.extra}
|
||||
if contact_in.extra != new_extra:
|
||||
if new_extra != contact_orm.extra:
|
||||
contact_orm.extra = new_extra
|
||||
updated = True
|
||||
|
||||
# 触发同步信息到星云
|
||||
if updated and not contact_orm.xingyun_sync and contact_orm.xingyun_id:
|
||||
try:
|
||||
await self.bind_xingyun_user(contact_orm)
|
||||
update_list.append(contact_orm)
|
||||
except Exception as e:
|
||||
logger.error(f'客户数据同步错误,xingyun_id: {contact_orm.xingyun_id}, {e}')
|
||||
continue
|
||||
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, list(field_names) + ['extra', 'xingyun_sync'])
|
||||
|
||||
await update(contact_dict, contact_orm_list, 'xingyun_id')
|
||||
|
||||
add_dict = {}
|
||||
for contact in contact_dict.values():
|
||||
externalUserid = contact.get('externalUserid', None)
|
||||
if not externalUserid:
|
||||
continue
|
||||
try:
|
||||
resp = await from_service_external_userid(wk_client, externalUserid)
|
||||
except Exception as e:
|
||||
logger.error(f'从星云加载客户数据,获取用户ID失败,externalUserid: {externalUserid}, {e}')
|
||||
continue
|
||||
if not resp or not resp.get('external_userid'):
|
||||
continue
|
||||
|
||||
external_userid = resp.get('external_userid')
|
||||
contact['external_userid'] = external_userid
|
||||
add_dict[external_userid] = contact
|
||||
|
||||
contact_orm_list = await self.model.filter(weixin_id__in=add_dict.keys())
|
||||
await update(add_dict, contact_orm_list, 'weixin_id')
|
||||
|
||||
add_list = []
|
||||
for contact in add_dict.values():
|
||||
contact_in = WeixinCustomerXingyunCreate(**contact).transform_to_extra()
|
||||
add_list.append(self.model(**contact_in.model_dump(exclude_unset=True)))
|
||||
|
||||
if add_list:
|
||||
await self.model.bulk_create(add_list)
|
||||
|
||||
return {
|
||||
'update_count': len(update_list),
|
||||
'add_count': len(add_list),
|
||||
}
|
||||
|
||||
async def bind_user(self, bind_info: WeixinUserBindInfo):
|
||||
return await self.bind_user(bind_info)
|
||||
|
||||
async def monitor_erp_order(self):
|
||||
# 从领淘ERP加载订单相关用户
|
||||
# 获取上一个订单的创建时间
|
||||
cache_file = 'env/order_cache.pkl'
|
||||
newest_order_create_time = None
|
||||
today_end = datetime.now().replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
try:
|
||||
with open(cache_file, 'rb') as f:
|
||||
last_order = pickle.load(f)
|
||||
newest_order_create_time = last_order.get('create_time')
|
||||
logger.info(f'从缓存文件加载最新订单创建时间,{newest_order_create_time}')
|
||||
except FileNotFoundError:
|
||||
newest_order_create_time = today_end.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
newest_order_create_time = newest_order_create_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
order_list = [o async for o in get_order_relative_user(lintao_client, start_date=newest_order_create_time, end_date=today_end.strftime('%Y-%m-%d %H:%M:%S'))]
|
||||
msg_list, newest_order = await self.find_bind_order(order_list)
|
||||
|
||||
# 保存最新订单到缓存文件
|
||||
if newest_order:
|
||||
newest_order['create_time'] = newest_order['create_time'].replace('T', ' ')
|
||||
with open(cache_file, 'wb') as f:
|
||||
pickle.dump(newest_order, f)
|
||||
|
||||
return msg_list
|
||||
|
||||
async def find_bind_order(self, order_list: list):
|
||||
|
||||
buyer_order_dict = {}
|
||||
newest_order = None
|
||||
last_erp_id = 0
|
||||
|
||||
order_id_set = set()
|
||||
for order in order_list:
|
||||
erp_id = order.get('id')
|
||||
if erp_id <= 6368700: break
|
||||
|
||||
create_time = order.get('create_time')
|
||||
if last_erp_id < erp_id:
|
||||
if create_time: newest_order = order
|
||||
last_erp_id = erp_id
|
||||
|
||||
order_id = order.get('trade_no')
|
||||
if order_id in order_id_set: continue
|
||||
order_id_set.add(order_id)
|
||||
|
||||
users = order.get("users", [])
|
||||
buyer_id_set = set()
|
||||
for user in users:
|
||||
role = user.get('role')
|
||||
if role != 'buyer': continue
|
||||
buyer_id = user.get('id')
|
||||
if not buyer_id: continue
|
||||
if buyer_id in buyer_id_set: continue
|
||||
buyer_id_set.add(buyer_id)
|
||||
|
||||
buyer_order_dict.setdefault(buyer_id, []).append(order)
|
||||
|
||||
customer_list = await self.model.filter(taobao_id__in=buyer_order_dict.keys()).all()
|
||||
|
||||
# 通知新订单
|
||||
# (客户id)-(所属企微客服)刚在(店铺名)下单-(金额)
|
||||
msg_set = set()
|
||||
msg_list = []
|
||||
for customer_orm in customer_list:
|
||||
order_list = buyer_order_dict.get(customer_orm.taobao_id, [])
|
||||
|
||||
order_info = [
|
||||
f"在({order.get('shop_name')})下单{order.get('price')}元,单号({order.get('trade_no')})"
|
||||
for order in order_list
|
||||
]
|
||||
chat_info = await get_external_user_chat_info(customer_orm.weixin_id)
|
||||
follow_user_list = chat_info.get('follow_user', [])
|
||||
|
||||
for follow_user in follow_user_list:
|
||||
follow_userid = follow_user.get('userid')
|
||||
weixin_user_orm: WeixinUser = await WeixinUser.filter(userid=follow_userid).first()
|
||||
owner_name = '未知'
|
||||
if weixin_user_orm: owner_name = weixin_user_orm.english_name or weixin_user_orm.username
|
||||
qiwei_customer_name = customer_orm.taobao_name or customer_orm.weixin_name
|
||||
|
||||
content = f"({qiwei_customer_name}) -({owner_name})- 刚{', '.join(order_info)}"
|
||||
msg_hash_id = hashlib.md5(content.encode('utf-8')).hexdigest()
|
||||
if msg_hash_id in msg_set: continue
|
||||
msg_set.add(msg_hash_id)
|
||||
|
||||
# 检查消息是否已存在
|
||||
msg_orm = await msg_controller.model.filter(hash_id=msg_hash_id).first()
|
||||
if msg_orm: continue
|
||||
|
||||
msg = msg_controller.model(
|
||||
hash_id=msg_hash_id,
|
||||
title=qiwei_customer_name,
|
||||
content=content,
|
||||
detail=order_list,
|
||||
type=MsgType.NEW_ORDER,
|
||||
owner_id=follow_userid,
|
||||
owner_name=owner_name
|
||||
)
|
||||
logger.info(f'创建新订单通知消息,{msg.content}')
|
||||
msg_list.append(msg)
|
||||
|
||||
if msg_list:
|
||||
await msg_controller.model.bulk_create(msg_list)
|
||||
|
||||
return msg_list, newest_order
|
||||
|
||||
async def get_customer_by_weixin_id(self, weixin_id: str):
|
||||
async with cache_if(f'customer:user_datail_0:{weixin_id}', ttl=3600*24) as cache:
|
||||
if cache.hit:
|
||||
result = cache.value
|
||||
return result
|
||||
else:
|
||||
result = await self._get_customer_by_weixin_id(weixin_id)
|
||||
cache.set(result)
|
||||
return result
|
||||
|
||||
async def _get_customer_by_weixin_id(self, weixin_id: str):
|
||||
chat_info = await get_external_user_chat_info(weixin_id)
|
||||
follow_user_list = chat_info.get('follow_user', [])
|
||||
external_contact = chat_info.get('external_contact', {})
|
||||
|
||||
if external_contact:
|
||||
customer_orm = await self.model.filter(weixin_id=weixin_id).first()
|
||||
if customer_orm:
|
||||
customer_orm.weixin_avatar = external_contact.get('avatar')
|
||||
customer_orm.weixin_unionid = external_contact.get('unionid')
|
||||
customer_orm.weixin_name = external_contact.get('name')
|
||||
await customer_orm.save()
|
||||
|
||||
user_ids = [follow_user.get('userid') for follow_user in follow_user_list]
|
||||
weixin_user_orm_list = await WeixinUser.filter(userid__in=user_ids).all()
|
||||
weixin_user_orm_dict = {weixin_user_orm.userid: weixin_user_orm for weixin_user_orm in weixin_user_orm_list}
|
||||
for follow_user in follow_user_list:
|
||||
follow_userid = follow_user.get('userid')
|
||||
weixin_user_orm = weixin_user_orm_dict.get(follow_userid)
|
||||
|
||||
if weixin_user_orm:
|
||||
follow_user['username'] = weixin_user_orm.english_name or weixin_user_orm.username
|
||||
|
||||
return {
|
||||
**external_contact,
|
||||
'follow_user_list': follow_user_list,
|
||||
}
|
||||
|
||||
# for follow_user in follow_user_list:
|
||||
# follow_userid = follow_user.get('userid')
|
||||
|
||||
async def get_user_detail(self, weixin_id: str):
|
||||
async with cache_if(f'customer:user_datail:{weixin_id}', ttl=3600) as cache:
|
||||
if cache.hit:
|
||||
result = cache.value
|
||||
return result
|
||||
else:
|
||||
customer = await weixin_customer_controller.model.get_or_none(weixin_id=weixin_id)
|
||||
# if customer: return customer.to_dict()
|
||||
print(f'get_user_detail, weixin_id: {weixin_id}, customer: {customer}')
|
||||
|
||||
# 通过中间表 CustomerGroup 查询其加入的所有群
|
||||
memberships = await CustomerGroup.filter(
|
||||
customer=customer
|
||||
).prefetch_related('group') # 预加载 group 信息
|
||||
|
||||
group_list = []
|
||||
result = {'group_list': group_list}
|
||||
for m in memberships:
|
||||
data = await m.to_dict()
|
||||
# data['group'] = await m.group.to_dict()
|
||||
data['chat_name'] = m.group.name
|
||||
data['create_time'] = m.group.create_time
|
||||
group_list.append(data)
|
||||
|
||||
chat_info = await self.get_customer_by_weixin_id(weixin_id)
|
||||
result['user_detail'] = chat_info
|
||||
cache.set(result)
|
||||
|
||||
return result
|
||||
|
||||
# 同步星云和数据库中的客户信息: 同步绑定信息到星云、从星云中加载订单信息
|
||||
async def sync_xingyun_contact_info(self, eager_load: bool = False):
|
||||
# 从星云同步绑定信息到本地
|
||||
await self._sync_xingyun_contact_info(eager_load=eager_load, order_id__isnull=False)
|
||||
# 从本地同步订单信息到星云
|
||||
await self._sync_xingyun_contact_info(eager_load=eager_load, order_id__isnull=True, limit=10)
|
||||
|
||||
async def _sync_xingyun_contact_info(self, eager_load: bool = False, max_try: int = 2, limit: int = 20, **query_kwargs):
|
||||
query_kwargs = query_kwargs or {}
|
||||
|
||||
last_id = 0
|
||||
while eager_load or max_try > 0:
|
||||
max_try -= 1
|
||||
if last_id:
|
||||
contact_orm_list = await self.model.filter(xingyun_sync=False, xingyun_id__isnull=False, id__lt=last_id, **query_kwargs).order_by('-id').limit(limit).all()
|
||||
else:
|
||||
contact_orm_list = await self.model.filter(xingyun_sync=False, xingyun_id__isnull=False, **query_kwargs).order_by('-id').limit(limit).all()
|
||||
|
||||
if not contact_orm_list: break
|
||||
logger.debug(f'同步客户数据,{len(contact_orm_list)}')
|
||||
|
||||
has_new = False
|
||||
update_list = []
|
||||
for contact_orm in contact_orm_list:
|
||||
last_id = contact_orm.id
|
||||
has_new = True
|
||||
|
||||
if contact_orm.need_confirm:
|
||||
logger.info(f'客户需要确认绑定: {contact_orm}')
|
||||
continue
|
||||
|
||||
if contact_orm.order_id:
|
||||
try:
|
||||
await self.bind_xingyun_user(contact_orm)
|
||||
update_list.append(contact_orm)
|
||||
except Exception as e:
|
||||
logger.error(f'同步客户订单信息到星云错误,xingyun_id: {contact_orm.xingyun_id}, {e}')
|
||||
continue
|
||||
else:
|
||||
# list_user_order
|
||||
try:
|
||||
order_list = list_user_order(xy_client, cid=contact_orm.xingyun_id)
|
||||
except Exception as e:
|
||||
logger.error(f'客户数据同步错误,xingyun_id: {contact_orm.xingyun_id}, {e}')
|
||||
logger.exception(e)
|
||||
continue
|
||||
|
||||
shopNameSet, order_id_list = set(), []
|
||||
async for order in order_list:
|
||||
shopName = order.get('shopName')
|
||||
if shopName in shopNameSet:
|
||||
continue
|
||||
shopNameSet.add(shopName)
|
||||
order_id_list.append(order.get('orderId'))
|
||||
# 多店铺用的用的名称和ID都一样,所以不区分
|
||||
break
|
||||
|
||||
for order_id in order_id_list:
|
||||
try:
|
||||
await self.bind_order(WeixinOrderBindInfo(
|
||||
userid=contact_orm.weixin_id,
|
||||
order_id=order_id,
|
||||
))
|
||||
except AssertionError as e:
|
||||
logger.error(f'同步客户订单信息到本地错误,xingyun_id: {contact_orm.xingyun_id}, {e}')
|
||||
logger.exception(e)
|
||||
continue
|
||||
contact_orm.xingyun_sync = True
|
||||
update_list.append(contact_orm)
|
||||
logger.info(f'同步客户订单信息到本地: {contact_orm.xingyun_id} -》 {order_id}')
|
||||
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, ['xingyun_sync'])
|
||||
|
||||
if not has_new: break
|
||||
|
||||
weixin_customer_controller = WeixinCustomerController()
|
||||
@@ -0,0 +1,408 @@
|
||||
from tortoise.expressions import Q
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.schemas.weixin import (
|
||||
WeixinGroupChatCreate,
|
||||
WeixinGroupChatUpdate,
|
||||
WeixinGroupChatXingyunCreate,
|
||||
)
|
||||
|
||||
from some_sdk.services.binder import wk_client, lintao_client, xy_client
|
||||
from some_sdk.wk_weixin_sdk.apis import corp_group
|
||||
from some_sdk.wk_weixin_sdk.apis.extern_user import get_external_user_chat_info
|
||||
from some_sdk.xingyun_sdk.apis.work_user import list_work_group
|
||||
from some_sdk.xingyun_sdk.apis.customer import list_user_join_group
|
||||
|
||||
from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user
|
||||
from some_sdk.lintao_sdk.apis.orderlist import list_product_raw, get_order_log
|
||||
|
||||
from app.models.weixin import WeixinGroupChat, RoleType, CustomerGroup
|
||||
from app.controllers.weixin.user import weixin_user_controller
|
||||
from app.controllers.weixin.customer import weixin_customer_controller
|
||||
from app.utils.common import async_split_generator, async_generator_to_list
|
||||
from app.core.cache import cache_if, invalidate_cache
|
||||
|
||||
import re
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ROLE_LIST = [role.value for role in RoleType]
|
||||
|
||||
class WeixinGroupChatController(CRUDBase[WeixinGroupChat, WeixinGroupChatCreate, WeixinGroupChatUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=WeixinGroupChat)
|
||||
|
||||
async def get_external_group_chat_info(self, chat_id: str):
|
||||
group_info = await corp_group.get_external_group_chat_info(wk_client, chat_id)
|
||||
|
||||
internal_member_list = group_info['internal_member_list']
|
||||
external_member_list = group_info['external_member_list']
|
||||
group_name = group_info.get('name')
|
||||
|
||||
external_member_list.sort(key=lambda user: user.get('join_time'))
|
||||
|
||||
unique_dict = {
|
||||
'name': group_name,
|
||||
'external_user_count': len(external_member_list),
|
||||
'internal_member_count': len(internal_member_list),
|
||||
}
|
||||
group_info.update(unique_dict)
|
||||
# 同步更新数据库
|
||||
status, obj = await self.create_or_update(group_info, query_kwargs={'chat_id': chat_id}, update_kwargs=unique_dict)
|
||||
if not status:
|
||||
logger.debug(f'群聊数据不变,不用更新,chat_id: {chat_id}')
|
||||
# elif status == 'create':
|
||||
# await self.create_customer_group(chat_id)
|
||||
else:
|
||||
# 群更新:触发数据同步事件
|
||||
# event_manager.subscribe("group_chat_updated", update_cache_handler, EventPriority.MEDIUM)
|
||||
|
||||
# 只保存准确无误的数据
|
||||
# buyer_nick = get_buyer_nick_from_group_name(group_name)
|
||||
# logger.debug(f'group_name: {group_name}; buyer_nick: {buyer_nick}')
|
||||
# if not buyer_nick:
|
||||
# logger.error(f'群聊未命名,无法绑定订单,chat_id: {chat_id}')
|
||||
# return group_info
|
||||
|
||||
# self.load_erp_order_info_by_group(buyer_nick, internal_member_list, external_member_list)
|
||||
|
||||
update_result1 = await weixin_user_controller.update_from_group([*internal_member_list])
|
||||
update_result2 = await weixin_customer_controller.update_from_group([*external_member_list])
|
||||
logger.debug(f'群聊用户数据更新结果,chat_id: {chat_id}, {update_result1}')
|
||||
logger.debug(f'群聊客户数据更新结果,chat_id: {chat_id}, {update_result2}')
|
||||
# 刷新用户缓存
|
||||
for user in [*internal_member_list, *external_member_list]:
|
||||
await self.expire_user_cache(user.get('userid'))
|
||||
|
||||
group_info['is_create'] = status == 'create'
|
||||
group_info['detect_update'] = status != False
|
||||
return group_info
|
||||
|
||||
# 从星云有客中加载群聊信息
|
||||
async def load_xingyun_group_info(self, chat_name: str=''):
|
||||
# 检索出最新的记录,只加载最新的记录
|
||||
newest_group = await self.model.filter(xingyun_chat_id__isnull=False).order_by('-create_time').first()
|
||||
newest_time = newest_group.create_time if newest_group else 1735660800 # 2025-01-01 00:00:00
|
||||
|
||||
load_result = {"update_count": 0, "add_count": 0}
|
||||
group_iter = list_work_group(xy_client, chatName=chat_name)
|
||||
async for group_list in async_split_generator(group_iter):
|
||||
if group_list[-1].get('createTime') < newest_time:
|
||||
break
|
||||
logger.debug(f'处理群聊信息,chat_name: {chat_name}, {len(group_list)}')
|
||||
result = await self.save_xingyun_group_info(group_list)
|
||||
load_result['update_count'] += result['update_count']
|
||||
load_result['add_count'] += result['add_count']
|
||||
logger.debug(f'群聊数据更新结果,chat_name: {chat_name}, {result}')
|
||||
|
||||
if load_result['add_count'] + load_result['update_count'] > 300:
|
||||
break
|
||||
|
||||
await self.refresh_xingyun_group_info()
|
||||
return load_result
|
||||
|
||||
async def save_xingyun_group_info(self, group_list: list):
|
||||
group_dict = {group.get('groupChatId'): group for group in group_list}
|
||||
group_orm_list = await self.model.filter(chat_id__in=group_dict.keys())
|
||||
|
||||
update_list = []
|
||||
history = {}
|
||||
for group_orm in group_orm_list:
|
||||
group = group_dict.pop(group_orm.chat_id, history.get(group_orm.chat_id, {}))
|
||||
if not group: continue
|
||||
history[group_orm.chat_id] = group
|
||||
|
||||
try:
|
||||
group_in = WeixinGroupChatXingyunCreate(**group)
|
||||
except Exception as e:
|
||||
logger.error(f'群聊数据转换错误,chat_id: {group}, {e}')
|
||||
continue
|
||||
updated = False
|
||||
if group_in.avatars:
|
||||
group_orm.avatars = group_in.avatars
|
||||
updated = True
|
||||
if group_orm.xingyun_chat_id != group_in.xingyun_chat_id:
|
||||
group_orm.xingyun_chat_id = group_in.xingyun_chat_id
|
||||
updated = True
|
||||
if updated:
|
||||
update_list.append(group_orm)
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, ['avatars', 'xingyun_chat_id'])
|
||||
|
||||
add_list = []
|
||||
for chat_id, group in group_dict.items():
|
||||
group_in = WeixinGroupChatXingyunCreate(**group)
|
||||
add_list.append(self.model(**group_in.model_dump(exclude_unset=True)))
|
||||
|
||||
if add_list:
|
||||
await self.model.bulk_create(add_list)
|
||||
|
||||
return {
|
||||
'update_count': len(update_list),
|
||||
'add_count': len(add_list),
|
||||
}
|
||||
|
||||
# 刷新数据库中的群聊信息
|
||||
async def refresh_xingyun_group_info(self):
|
||||
|
||||
for _ in range(10):
|
||||
group_orm_list = await self.model.filter(xingyun_chat_id__isnull=False, external_user_count__isnull=True).order_by('-create_time').limit(20).all()
|
||||
if not group_orm_list: break
|
||||
logger.debug(f'刷新群聊数据,{len(group_orm_list)}')
|
||||
|
||||
for group_orm in group_orm_list:
|
||||
try:
|
||||
await self.get_external_group_chat_info(group_orm.chat_id)
|
||||
except Exception as e:
|
||||
logger.error(f'刷新群聊数据错误,chat_id: {group_orm.chat_id}, {e}')
|
||||
continue
|
||||
logger.debug(f'刷新群聊数据结果,chat_id: {group_orm.name} 已更新')
|
||||
|
||||
# 同步erp中的用户和客户信息到数据库
|
||||
async def load_erp_order_info_by_group(self, buyer_nick: str, internal_member_list: list, external_member_list: list):
|
||||
if not buyer_nick:
|
||||
logger.error(f'群聊未命名,无法绑定订单,buyer_nick: {buyer_nick}')
|
||||
return
|
||||
|
||||
orders = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_nick=buyer_nick))
|
||||
assert orders, f'未找到订单,buyer_nick: {buyer_nick}'
|
||||
|
||||
internal_member_dict = {user.get('name'): user for user in internal_member_list}
|
||||
|
||||
order = orders[0]
|
||||
|
||||
order_relative_user = order.get("users", [])
|
||||
order_relative_user_dict = {user.get('name'): user for user in order_relative_user}
|
||||
|
||||
for name, user in order_relative_user_dict.items():
|
||||
erp_id = user.get('id', '')
|
||||
if not erp_id: continue
|
||||
if re.match(r'^\d+$', erp_id) is None:
|
||||
# 客户
|
||||
if not external_member_list:
|
||||
logger.error(f'客户群聊用户数为0,无法绑定客户, name: {name}, erp_id: {erp_id}')
|
||||
continue
|
||||
if name != buyer_nick:
|
||||
logger.error(f'客户群聊用户与ERP卖家名称不一致,无法绑定客户, name: {name}, erp_id: {erp_id}')
|
||||
continue
|
||||
if len(external_member_list) != 1:
|
||||
external_member_list.sort(key=lambda user: user.get('join_time'))
|
||||
logger.warning(f'客户群聊用户数不是1个,将选取最先入群的用户作为客户,name: {name}, erp_id: {erp_id}')
|
||||
|
||||
for member in external_member_list:
|
||||
member['order_id'] = order.get('trade_no')
|
||||
|
||||
weixin_user = external_member_list[0]
|
||||
weixin_user.update({
|
||||
'order_id': order.get('trade_no'),
|
||||
'taobao_id': user.get('id', ''),
|
||||
'taobao_name': user.get('name', ''),
|
||||
'erp_message': user.get('message', ''),
|
||||
'erp_remark': user.get('memo', ''),
|
||||
'need_confirm': len(external_member_list) > 1,
|
||||
})
|
||||
|
||||
continue
|
||||
else:
|
||||
# 员工
|
||||
erp_id = int(erp_id)
|
||||
weixin_user = internal_member_dict.get(name)
|
||||
logger.debug(f'name: {name}, erp_id: {erp_id}, weixin_user: {weixin_user}')
|
||||
if not weixin_user:
|
||||
logger.error(f'{internal_member_dict.keys()} {name}')
|
||||
logger.error(f'客户群聊用户与ERP员工名称不一致,无法绑定员工, name: {name}, erp_id: {erp_id}')
|
||||
continue
|
||||
weixin_user.update({
|
||||
'erp_id': erp_id,
|
||||
'erp_name': name,
|
||||
'role': user.get('role', ''),
|
||||
})
|
||||
continue
|
||||
|
||||
# 通过客户ID获取客户的所有订单
|
||||
async def get_order_relative_user_list_by_weixin_userid(self, user_id: str):
|
||||
customers = await weixin_customer_controller.model.filter(weixin_id=user_id)
|
||||
if not customers:
|
||||
await weixin_customer_controller.create({
|
||||
"weixin_id": user_id,
|
||||
})
|
||||
# 新用户:触发数据同步事件
|
||||
return False, []
|
||||
|
||||
order_list = []
|
||||
buyer_set = set()
|
||||
for customer in customers:
|
||||
buyer_nick = customer.taobao_name
|
||||
buyer_id = customer.taobao_id
|
||||
if buyer_nick:
|
||||
logger.debug(f'name: {user_id}; buyer_nick: {buyer_nick}')
|
||||
if buyer_nick in buyer_set: continue
|
||||
buyer_set.add(buyer_nick)
|
||||
orders = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_nick=buyer_nick))
|
||||
elif buyer_id:
|
||||
if buyer_id in buyer_set: continue
|
||||
buyer_set.add(buyer_id)
|
||||
orders = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_id=buyer_id))
|
||||
else:
|
||||
assert buyer_nick, "当前客户未绑定淘宝账号"
|
||||
|
||||
logger.debug(f'orders.length: {len(orders)}')
|
||||
order_list.extend(orders)
|
||||
|
||||
# for order in orders:
|
||||
# 原地按照id排序
|
||||
order_list.sort(key=lambda order: order.get('id'), reverse=True)
|
||||
if order_list:
|
||||
await self.get_user_detail_by_order(order=order_list[0])
|
||||
|
||||
return True, order_list
|
||||
|
||||
# 根据客户名字,获取群聊中客户的所有订单
|
||||
async def get_order_relative_user_list_by_weixin_group_name(self, buyer_nick: str):
|
||||
async with cache_if(f'order:relative_user:{buyer_nick}', ttl=10) as cache:
|
||||
if cache.hit:
|
||||
order_list = cache.value
|
||||
logger.debug(f'{cache.key} hit')
|
||||
else:
|
||||
logger.debug(f'{cache.key} not hit')
|
||||
order_list = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_nick=buyer_nick))
|
||||
cache.set(order_list)
|
||||
logger.debug(f'order_list.length: {len(order_list)}')
|
||||
|
||||
# for order in order_list:
|
||||
order_list.sort(key=lambda order: order.get('id'), reverse=True)
|
||||
if order_list:
|
||||
await self.get_user_detail_by_order(order=order_list[0])
|
||||
order_list[0]['__type__'] = 'load_order_by_weixin_group_name'
|
||||
|
||||
return order_list
|
||||
|
||||
# 根据客户ID,获取群聊中客户的所有订单
|
||||
async def get_order_relative_user_list_by_weixin_group_buyer_id(self, buyer_id: str):
|
||||
|
||||
orders = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_id=buyer_id))
|
||||
logger.debug(f'orders.length: {len(orders)}')
|
||||
|
||||
# for order in orders:
|
||||
if orders:
|
||||
await self.get_user_detail_by_order(order=orders[0])
|
||||
|
||||
return orders
|
||||
|
||||
async def get_user_detail_by_order(self, order_id: str='', order: dict = None):
|
||||
if not order:
|
||||
orders = await async_generator_to_list(get_order_relative_user(lintao_client, trade_no=order_id))
|
||||
if not orders: return
|
||||
_orders = [o for o in orders if o.get('ctid') == order_id]
|
||||
# assert len(orders) == 1, f'订单号 {order_id} 对应多个订单'
|
||||
if not _orders:
|
||||
for order in orders:
|
||||
logger.warning(f'订单号 {order_id} 对应多个订单,{order}')
|
||||
# return {}
|
||||
_orders = orders
|
||||
|
||||
order = _orders[0]
|
||||
|
||||
# 缓存订单产品信息
|
||||
tid = order.get("ctid")
|
||||
async with cache_if(f'order:product:{tid}', ttl=3600*24) as cache:
|
||||
if cache.hit:
|
||||
product_resp = cache.value
|
||||
else:
|
||||
product_resp = (await list_product_raw(lintao_client, tid=tid)).get('data', {})
|
||||
if product_resp: cache.set(product_resp)
|
||||
|
||||
if product_resp:
|
||||
order['product_title'] = product_resp[0].get("title")
|
||||
order['product_pic'] = product_resp[0].get("pic_path")
|
||||
|
||||
order['has_fetched_user_detail'] = True
|
||||
user_list = order.get("users", [])
|
||||
|
||||
async with cache_if(f'user:auto_relate', ttl=600) as cache:
|
||||
if cache.hit:
|
||||
default_user_list = cache.value
|
||||
else:
|
||||
default_user_list = await weixin_user_controller.all(Q(auto_relate=True))
|
||||
default_user_list = [user.to_dict() for user in default_user_list]
|
||||
cache.set(default_user_list)
|
||||
default_user_set = {str(user.get('erp_id')) for user in default_user_list}
|
||||
|
||||
for user in user_list:
|
||||
erp_id = user.get('id', '')
|
||||
if erp_id in default_user_set: continue
|
||||
await self.get_order_user_info(user)
|
||||
|
||||
order['users'].extend(default_user_list)
|
||||
|
||||
# 根据 role_list 中的顺序进行排序
|
||||
role_order = {role: index for index, role in enumerate(ROLE_LIST)}
|
||||
order['users'].sort(key=lambda user: role_order.get(user['role'], len(ROLE_LIST)))
|
||||
|
||||
return order
|
||||
|
||||
async def get_order_user_info(self, user: dict=None):
|
||||
if not user: return
|
||||
erp_id = user.get('id', '')
|
||||
if not erp_id: return
|
||||
if re.match(r'^\d+$', erp_id) is None:
|
||||
# logger.debug(f'淘宝客户, erp_id: {erp_id}, user: {user}')
|
||||
# 客户
|
||||
taobao_id = user.get('id', '')
|
||||
query = Q(taobao_id=taobao_id)
|
||||
weixin_customer_list = await weixin_customer_controller.all(query)
|
||||
if not weixin_customer_list:
|
||||
logger.error(f'客户未绑定订单, taobao_id: {taobao_id}, user: {user}')
|
||||
return
|
||||
weixin_customer = weixin_customer_list[0]
|
||||
buyer = weixin_customer.to_dict()
|
||||
if not buyer.get('name') and user.get('name'):
|
||||
weixin_customer.taobao_name = user.get('name')
|
||||
await weixin_customer.save()
|
||||
user.update({**buyer, **user})
|
||||
else:
|
||||
# 员工
|
||||
erp_id = int(erp_id)
|
||||
# logger.debug(f'员工, erp_id: {erp_id}, user: {user}')
|
||||
query = Q(erp_id=erp_id)
|
||||
weixin_user_list = await weixin_user_controller.all(query)
|
||||
if not weixin_user_list:
|
||||
user['erp_id'] = erp_id
|
||||
logger.error(f'员工未绑定ERP, erp_id: {erp_id}, user: {user}')
|
||||
return
|
||||
weixin_user = weixin_user_list[0]
|
||||
user.update(weixin_user.to_dict())
|
||||
|
||||
# 获取erp日志
|
||||
async def get_erp_order_log(self, order_id: str):
|
||||
return await get_order_log(lintao_client, order_id)
|
||||
|
||||
async def expire_user_cache(self, weixin_id: str):
|
||||
await invalidate_cache(f'customer:user_datail:{weixin_id}')
|
||||
await invalidate_cache(f'customer:user_datail_0:{weixin_id}')
|
||||
|
||||
# async def list_user_join_group(self, xingyun_id: int):
|
||||
# assert xingyun_id, 'id 不能为空'
|
||||
# result = await async_generator_to_list(list_user_join_group(xy_client, cid=xingyun_id))
|
||||
|
||||
# xingyun_chat_ids = [item.get('id') for item in result]
|
||||
# all_group_chat = await self.model.filter(xingyun_chat_id__in=xingyun_chat_ids)
|
||||
# group_chat_dict = {item.xingyun_chat_id: item for item in all_group_chat}
|
||||
# logger.debug(f'list_user_join_group, xingyun_chat_ids: {xingyun_chat_ids}')
|
||||
|
||||
# remain_chat_ids = set()
|
||||
# for item in result:
|
||||
# group_chat_id = item.get('id')
|
||||
# if group_chat_id in group_chat_dict: continue
|
||||
# remain_chat_ids.add(group_chat_id)
|
||||
|
||||
# chat_name = item.get('chatName')
|
||||
# if not chat_name: continue
|
||||
# await self.load_xingyun_group_info(chat_name=chat_name)
|
||||
|
||||
# remain_chat_list = await self.model.filter(xingyun_chat_id__in=remain_chat_ids)
|
||||
# all_group_chat.extend(remain_chat_list)
|
||||
# return all_group_chat
|
||||
|
||||
weixin_group_chat_controller = WeixinGroupChatController()
|
||||
@@ -0,0 +1,169 @@
|
||||
from .customer import weixin_customer_controller
|
||||
from .user import weixin_user_controller
|
||||
from app.controllers.msg import msg_controller
|
||||
from app.controllers.action import action_controller
|
||||
from app.schemas.weixin import WeixinGroupChatEvent, BindOrderResultEvent, CustomerRepeatPurchaseEvent, CustomerAssignOrderEvent, CustomerRefundOrderEvent
|
||||
from app.utils.common import transform_pydantic_to_list
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from app.utils.event_task import event_manager, EventType
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
from typing import Any
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler as BackgroundScheduler
|
||||
|
||||
from app.controllers.automation.scenario import automation_scenario_controller
|
||||
from app.controllers.automation.task import task_controller
|
||||
|
||||
|
||||
sync_lock = asyncio.Lock()
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def sync_xingyun_contact_info_async(event_type, *args, **kwargs):
|
||||
if os.getenv("APP_ENV") != "prod":
|
||||
logger.info(f'非生产环境,不实际同步客户数据,{args} {kwargs}')
|
||||
return
|
||||
|
||||
logger.info(f'同步客户数据,{args} {kwargs}')
|
||||
now_time = datetime.now()
|
||||
start_time = (now_time - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_time = now_time + timedelta(days=1)
|
||||
await weixin_customer_controller.load_user_from_xingyun(add_time_start=start_time, add_time_end=end_time)
|
||||
|
||||
async def load_all_user_from_xingyun(event_type, *args, **kwargs):
|
||||
if os.getenv("APP_ENV") != "prod":
|
||||
logger.info(f'非生产环境,不实际从星云加载全量客户数据,{args} {kwargs}')
|
||||
return
|
||||
|
||||
logger.info(f'从星云加载全量客户数据,{args} {kwargs}')
|
||||
start_time = datetime(2025, 6, 1)
|
||||
end_time = datetime.now() + timedelta(days=1)
|
||||
return await weixin_customer_controller.load_all_user_from_xingyun(task_id='load_all_user_from_xingyun', add_time_start=start_time, add_time_end=end_time)
|
||||
|
||||
async def monitor_erp_order(event_type, *args, **kwargs):
|
||||
if os.getenv("APP_ENV") != "prod":
|
||||
logger.info(f'非生产环境,不实际从星云加载全量客户数据,{args} {kwargs}')
|
||||
return
|
||||
|
||||
now_time = datetime.now()
|
||||
logger.info(f'监控ERP订单,{args} {kwargs}')
|
||||
new_order_list = [1]
|
||||
new_order_list = await weixin_customer_controller.monitor_erp_order()
|
||||
if new_order_list:
|
||||
logger.info(f'监控到新订单,{new_order_list}')
|
||||
await msg_controller.sync_msg()
|
||||
logger.debug(f'耗时:{datetime.now() - now_time}')
|
||||
|
||||
async def update_erp_order(*args, **kwargs):
|
||||
logger.info(f'更新ERP订单,{args} {kwargs}')
|
||||
await msg_controller.get_order_user_days_before(2)
|
||||
|
||||
async def check_remark_action_is_done(*args, **kwargs):
|
||||
if os.getenv("APP_ENV") != "prod":
|
||||
logger.info(f'非生产环境,不实际检查备注操作是否完成,{args} {kwargs}')
|
||||
return
|
||||
|
||||
logger.info(f'检查备注操作是否完成,{args} {kwargs}')
|
||||
await action_controller.check_remark_action_is_done()
|
||||
|
||||
# 每天到点执行一次
|
||||
today = datetime.now()
|
||||
todayStart = today.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
if today.hour in [23] and today.minute >= 50:
|
||||
todayStart += timedelta(days=1)
|
||||
class_type = '早班'
|
||||
elif today.hour in [0, 1] and today.minute <= 10:
|
||||
class_type = '早班'
|
||||
elif today.hour in [16] and today.minute >= 50 or today.hour in [17,18] and today.minute <= 10:
|
||||
class_type = '晚班'
|
||||
else:
|
||||
return
|
||||
|
||||
date = todayStart.strftime('%Y-%m-%d')
|
||||
title = f'设置 {date} {class_type} 的员工活码'
|
||||
success_title = title+' 成功'
|
||||
fail_title = title+' 失败'
|
||||
|
||||
async with sync_lock:
|
||||
has_set = await msg_controller.model.filter(title__in=[success_title]).first()
|
||||
if has_set:
|
||||
logger.info(f'【已操作】{title}')
|
||||
return
|
||||
|
||||
logger.info(f'【开始】{title}')
|
||||
try:
|
||||
result, next_class = await weixin_user_controller.set_user_online(class_type=class_type, date=todayStart)
|
||||
except Exception as e:
|
||||
logger.exception(f'设置 {date} {class_type} 的员工活码失败,{e}')
|
||||
result, next_class = {}, None
|
||||
|
||||
is_failed = bool(list(filter(lambda x: x.get('fail') != 0, result.values()))) or not result
|
||||
logger.info(f'【完成】{title},结果:{result} {"失败" if is_failed else "成功"}')
|
||||
|
||||
result_str = '\n'.join([f'{k}:{v}' for k, v in result.items()])
|
||||
|
||||
logger.info(f'{success_title if not is_failed else fail_title},{result_str}')
|
||||
content = f'\n\n{result_str}'
|
||||
if result and not next_class:
|
||||
content += f'\n\n⚠️ 未设置下一班值班人员,请及时设置'
|
||||
logger.info(f'消息通知:{content}')
|
||||
await msg_controller.new_system_msg(title=success_title if not is_failed else fail_title, content=content)
|
||||
await msg_controller.sync_msg()
|
||||
|
||||
async def group_chat_update(event_type, group_info, *args):
|
||||
if os.getenv("APP_ENV") != "prod":
|
||||
logger.info(f'非生产环境,不实际检查群聊更新,{group_info} {args}')
|
||||
return
|
||||
|
||||
group_event = WeixinGroupChatEvent(**group_info)
|
||||
await sync_xingyun_contact_info_async()
|
||||
|
||||
|
||||
async def test_event(event_type, event_data=None, *args, **kwargs):
|
||||
# logger.info(f'测试事件,{args} {kwargs}')
|
||||
# scenario_list = await automation_scenario_controller.find_active_scenarios(event_type, event_data=event_data)
|
||||
# logger.info(f'触发事件,{len(scenario_list)}')
|
||||
# for scenario in scenario_list:
|
||||
# task = await task_controller.create_from_scenario(scenario=scenario, event_data=event_data)
|
||||
# logger.info(f'创建任务,{task}')
|
||||
pass
|
||||
|
||||
async def trigger_event(event_type: str, event_data: Any):
|
||||
"""解析事件数据"""
|
||||
logger.info(f'测试事件,{event_type} {event_data}')
|
||||
scenario_list = await automation_scenario_controller.find_active_scenarios(event_type, event_data=event_data)
|
||||
logger.info(f'触发事件,{len(scenario_list)}')
|
||||
for scenario, reason in scenario_list:
|
||||
logger.info(f'触发场景,{scenario},{reason}')
|
||||
try:
|
||||
task = await task_controller.create_from_scenario(scenario=scenario, event_data=event_data, reason=reason)
|
||||
logger.info(f'创建任务,{task}')
|
||||
except Exception as e:
|
||||
logger.error(f'创建任务失败,{e}')
|
||||
|
||||
# print(f'事件订阅:{transform_pydantic_to_list(WeixinGroupChatEvent)}')
|
||||
|
||||
event_manager.subscribe(EventType.SYNC_XINGYUN_CONTACT_INFO, load_all_user_from_xingyun)
|
||||
event_manager.subscribe(EventType.OPEN_PERSONAL_CHAT, sync_xingyun_contact_info_async)
|
||||
event_manager.subscribe(EventType.GROUP_CHAT_UPDATED, group_chat_update, input_model=transform_pydantic_to_list(WeixinGroupChatEvent))
|
||||
event_manager.subscribe(EventType.BIND_ORDER_FOR_USER, sync_xingyun_contact_info_async, input_model=transform_pydantic_to_list(BindOrderResultEvent))
|
||||
event_manager.subscribe(EventType.OLD_CUSTOMER_REPEAT_PURCHASE, test_event, input_model=transform_pydantic_to_list(CustomerRepeatPurchaseEvent))
|
||||
event_manager.subscribe(EventType.CUSTOMER_SERVICE_ASSIGN_ORDER, test_event, input_model=transform_pydantic_to_list(CustomerAssignOrderEvent))
|
||||
event_manager.subscribe(EventType.DESIGNER_ASSIGN_ORDER, test_event, input_model=transform_pydantic_to_list(CustomerAssignOrderEvent))
|
||||
event_manager.subscribe(EventType.CUSTOMER_RETURN_ORDER, test_event, input_model=transform_pydantic_to_list(CustomerRefundOrderEvent))
|
||||
event_manager.subscribe(EventType.DESIGNER_UPLOAD_DESIGN, test_event)
|
||||
|
||||
event_manager.register_pretask(trigger_event)
|
||||
|
||||
if os.getenv("APP_ENV") == "prod":
|
||||
logger.info('生产环境,启动定时任务')
|
||||
# 创建调度器
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
# scheduler.add_job(monitor_erp_order, "interval", seconds=60) # 每90秒执行一次
|
||||
scheduler.add_job(update_erp_order, "interval", seconds=240) # 每90秒执行一次
|
||||
scheduler.add_job(check_remark_action_is_done, "interval", seconds=180) # 每90秒执行一次
|
||||
scheduler.start()
|
||||
@@ -0,0 +1,201 @@
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.schemas.weixin import (
|
||||
WeixinUserCreate,
|
||||
WeixinUserUpdate,
|
||||
)
|
||||
|
||||
from app.models.weixin import WeixinUser
|
||||
from app.schemas.weixin import WeixinUserBindInfo
|
||||
|
||||
from some_sdk.services.binder import feishu_client, xy_client
|
||||
from some_sdk.feishu_sdk.biz.doc import search_file_record
|
||||
from some_sdk.xingyun_sdk.apis.channel import list_channel_group, list_channel_group_user_list, set_online_staff
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
def split_generator(iterable, chunk_size=50):
|
||||
"""
|
||||
将可迭代对象拆分为多个子列表
|
||||
"""
|
||||
chunk = []
|
||||
for item in iterable:
|
||||
chunk.append(item)
|
||||
if len(chunk) == chunk_size:
|
||||
yield chunk
|
||||
chunk = []
|
||||
if chunk:
|
||||
yield chunk
|
||||
|
||||
class WeixinUserController(CRUDBase[WeixinUser, WeixinUserCreate, WeixinUserUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=WeixinUser)
|
||||
|
||||
async def update_from_group(self, internal_members: list[dict]):
|
||||
# 同步更新数据库
|
||||
member_dict = {}
|
||||
for member in internal_members:
|
||||
member = member.copy()
|
||||
userid = member.get('userid', None)
|
||||
member['alias'] = member.pop('name', None)
|
||||
member_dict[userid] = member
|
||||
|
||||
member_list = await self.model.filter(userid__in=member_dict.keys())
|
||||
|
||||
update_list = []
|
||||
update_fileds = ['alias', 'role', 'erp_id', 'erp_name']
|
||||
|
||||
history = {}
|
||||
for member in member_list:
|
||||
member_info = member_dict.pop(member.userid, history.get(member.userid, {}))
|
||||
if member.role in ['owner', 'admin', 'follower']:
|
||||
continue
|
||||
if not member_info: continue
|
||||
history[member.userid] = member_info
|
||||
|
||||
updated = {}
|
||||
for field in update_fileds:
|
||||
old_value = getattr(member, field)
|
||||
if field not in member_info:
|
||||
continue
|
||||
value = member_info.get(field)
|
||||
if old_value != value:
|
||||
setattr(member, field, value)
|
||||
updated[f'{field}:{str(old_value)}'] = value
|
||||
if updated:
|
||||
logger.info(f'用户 {member.userid} 更新关系为 {updated}')
|
||||
update_list.append(member)
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, fields=update_fileds)
|
||||
|
||||
# 新增用户
|
||||
create_list = []
|
||||
for userid, member_info in member_dict.items():
|
||||
create_list.append(WeixinUser(
|
||||
**member_info,
|
||||
name=member_info.get('alias'),
|
||||
))
|
||||
if create_list:
|
||||
await self.model.bulk_create(create_list)
|
||||
|
||||
return {
|
||||
'update_count': len(update_list),
|
||||
'create_count': len(create_list),
|
||||
}
|
||||
|
||||
|
||||
async def bind_user(self, user_in: WeixinUserBindInfo):
|
||||
if not user_in.userid:
|
||||
raise ValueError("用户ID不能为空")
|
||||
if user_in.erp_id == 0:
|
||||
del user_in.erp_id
|
||||
|
||||
user = await self.model.filter(userid=user_in.userid).first()
|
||||
if not user:
|
||||
await self.create({**user_in.model_dump(exclude_unset=True), 'name': user_in.username})
|
||||
return {}
|
||||
await self.model.filter(userid=user_in.userid).update(**user_in.model_dump(exclude_unset=True))
|
||||
|
||||
async def set_user_online(self, class_type: str, date: datetime):
|
||||
daysTimestamp = lambda nowStamp, days: nowStamp + days * 24 * 60 * 60 * 1000
|
||||
formatDate = lambda ts: datetime.fromtimestamp(ts / 1000).strftime('%Y-%m-%d')
|
||||
|
||||
today = int(date.timestamp() * 1000)
|
||||
logger.info(f'设置 {formatDate(today)} 班次为 {class_type} 的员工为在线状态')
|
||||
yestoday = daysTimestamp(today, -1)
|
||||
tomorrow = daysTimestamp(today, 1)
|
||||
|
||||
# 查询日期为 yestoday 到 后天 之间的所有记录
|
||||
resp = search_file_record(
|
||||
feishu_client,
|
||||
app_token="V4qebMG2kamCWMslJmdcQMbtngW",
|
||||
table_id="tblKgjc3F2jZ2inC",
|
||||
filter={
|
||||
"conjunction": "and",
|
||||
"conditions": [{
|
||||
"field_name": "日期",
|
||||
"operator": "isGreater",
|
||||
"value": ["ExactDate", yestoday]
|
||||
},{
|
||||
"field_name": "日期",
|
||||
"operator": "isLess",
|
||||
"value": ["ExactDate", daysTimestamp(today, 2)]
|
||||
}]
|
||||
},
|
||||
)
|
||||
|
||||
set_result = {}
|
||||
current_class = (class_type, today)
|
||||
next_class = ('早班', tomorrow) if class_type == '晚班' else ('晚班', today)
|
||||
|
||||
current_class_mapping = {}
|
||||
next_class_mapping = {}
|
||||
async for data in resp:
|
||||
user = data.get('data', {})
|
||||
user_online = ((user.get('班次') or '')[:2], user.get('日期', ''))
|
||||
# logger.info(f'用户{user.get("接量成员", "")} 在线时间为{user_online} 当前班次为{current_class} 下一班次为{next_class}')
|
||||
class_group = user.get('组别', '')
|
||||
class_mapping = None
|
||||
# 当前班次
|
||||
if user_online == current_class:
|
||||
class_mapping = current_class_mapping
|
||||
set_result[class_group] = {"接量成员": user.get('接量成员', '')}
|
||||
# 下一班次
|
||||
elif user_online == next_class:
|
||||
class_mapping = next_class_mapping
|
||||
if class_mapping is None:
|
||||
continue
|
||||
|
||||
if user.get('id', ''):
|
||||
class_mapping.setdefault(class_group, [])
|
||||
class_mapping[class_group].append(user.get('id', ''))
|
||||
|
||||
assert current_class_mapping, f"根据日期{current_class[1]} {formatDate(current_class[1])}没有找到班次为{class_type}的员工"
|
||||
logger.info(f'根据日期{current_class[1]} {formatDate(current_class[1])} 找到班次为{class_type}的员工: {current_class_mapping} {set_result}')
|
||||
if next_class_mapping:
|
||||
logger.info(f'根据日期{next_class[1]} {formatDate(next_class[1])} 找到班次为{next_class[0]}的员工: {next_class_mapping}')
|
||||
|
||||
group_list = list_channel_group(xy_client)
|
||||
async for group in group_list:
|
||||
title = group.get("title", "")
|
||||
if title not in current_class_mapping:
|
||||
continue
|
||||
|
||||
set_result[title] = set_result.get(title) or {}
|
||||
try:
|
||||
user_list = list_channel_group_user_list(xy_client, groupId=group.get("id", ""))
|
||||
|
||||
huomaList = []
|
||||
async for user in user_list:
|
||||
huomaList.append(user.get('id'))
|
||||
|
||||
if not huomaList:
|
||||
logger.error(f'组 {title} {group.get("id", "")} 中没有员工')
|
||||
continue
|
||||
|
||||
is_faild, is_success = [], []
|
||||
# 按照50个一组设置在线状态
|
||||
for i in range(0, len(huomaList), 50):
|
||||
result = await set_online_staff(xy_client, allDayUserIds=','.join(current_class_mapping[title]), huomaList=huomaList[i:i+50])
|
||||
|
||||
is_faild += list(filter(lambda x: x.get('errorMsg', ''), result.get('data', [])))
|
||||
is_success += list(filter(lambda x: x.get('success', True), result.get('data', [])))
|
||||
|
||||
is_faild = set(is_faild)
|
||||
if is_faild: logger.error(f'设置失败的员工: {is_faild}')
|
||||
else: logger.info(f'全部员工设置成功')
|
||||
|
||||
set_result[title] = {
|
||||
'success': len(is_success),
|
||||
'fail': len(is_faild),
|
||||
**set_result[title]
|
||||
}
|
||||
except Exception as e:
|
||||
logger.exception(f'设置群 {group.get("id", "")} 中员工 {huomaList} 为在线状态失败: {e}')
|
||||
|
||||
return set_result, next_class_mapping
|
||||
|
||||
weixin_user_controller = WeixinUserController()
|
||||
@@ -0,0 +1,4 @@
|
||||
def get_buyer_nick_from_group_name(group_name: str):
|
||||
buyer_nick = group_name.rsplit('-', 1)[0] if '服务群' in group_name else group_name
|
||||
return buyer_nick
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from some_sdk.weixin_sdk.session import get_logged_in_client
|
||||
from some_sdk.weixin_sdk.apis.userlist import list_department_user, get_user_detail_by_vid
|
||||
|
||||
# 首次调用:登录并创建 client
|
||||
client = get_logged_in_client()
|
||||
|
||||
def test_list_department_user(partyid: str):
|
||||
department_list = list_department_user(client, partyid)
|
||||
for index, department in enumerate(department_list, 1):
|
||||
print(f'{index}. {department}')
|
||||
print('==== ' * 10)
|
||||
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_list_department_user('1688852859949799')
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
from some_sdk.xingyun_sdk.session import get_logged_in_client
|
||||
from some_sdk.xingyun_sdk.biz.by_user import get_user_order
|
||||
from .wechat_sdk import get_external_group_chat_info
|
||||
from app.models.weixin import WeixinUser
|
||||
|
||||
# 首次调用:登录并创建 client
|
||||
client = get_logged_in_client()
|
||||
|
||||
def get_user_order_list(group_name: str="", keyword: str="", regrex: str=None, userIds: list = None):
|
||||
result = get_user_order(client, keyword=keyword, group_name=group_name, regrex=regrex, userIds=userIds)
|
||||
if not result:
|
||||
return {}
|
||||
|
||||
order_list = result.pop("order_list", [])
|
||||
print(f'在群聊 {result["group_name"]} 中的客户 {result["name"]}({result["keyword"]}) 有订单数: {len(order_list)}')
|
||||
|
||||
subTradeNo_list = []
|
||||
for order in order_list:
|
||||
result_order = dict(tradeNo='', tradeTime=order.get("tradeTime", ''), payTime=order.get("payTime", ''))
|
||||
orderList = order.get("orderList", [])
|
||||
|
||||
for item in orderList:
|
||||
result_order['shopName'] = order.get("shopName", "")
|
||||
result_order['goodsName'] = item.get("goodsName", "")
|
||||
result_order['actuPayment'] = order.get("actuPayment", "")
|
||||
result_order['goodsCount'] = order.get("goodsCount", "")
|
||||
result_order['tradeNo'] = item.get("subTradeNo", "")
|
||||
result_order['pic'] = item.get("pic", "")
|
||||
result_order['other_status'] = order.get("orderStatus", 0)
|
||||
result_order['other_status_name'] = order.get("status_name", "")
|
||||
subTradeNo_list.append(result_order.copy())
|
||||
|
||||
import json; print(json.dumps(subTradeNo_list, indent=4, ensure_ascii=False))
|
||||
result['order_list'] = subTradeNo_list
|
||||
return result
|
||||
|
||||
async def get_user_order_list_by_chatid(chat_id: str):
|
||||
group_chat_info = await get_external_group_chat_info(chat_id)
|
||||
external_member_list = group_chat_info.get('external_member_list', [])
|
||||
|
||||
trade_list = []
|
||||
group_chat_info['trade_list'] = trade_list
|
||||
|
||||
invitor = group_chat_info.get('owner')
|
||||
qiwei_kefu = await WeixinUser.filter(userid=invitor).first()
|
||||
print(f'邀请人:{invitor} {qiwei_kefu.username} {qiwei_kefu.crmid}')
|
||||
|
||||
group_name = group_chat_info.get('name')
|
||||
# if not group_name:
|
||||
# group_name = '未知群聊'
|
||||
|
||||
print(f'群聊名称{group_name},有{len(external_member_list)}位客户', flush=True)
|
||||
for user in external_member_list:
|
||||
print(f'正在处理客户:{user.get("name")}', flush=True)
|
||||
result = get_user_order_list(group_name=group_name, keyword=user.get('name'), userIds=[qiwei_kefu.crmid] if qiwei_kefu.crmid else None)
|
||||
if not result:
|
||||
continue
|
||||
|
||||
trade_list.append(result)
|
||||
|
||||
return group_chat_info
|
||||
|
||||
if __name__ == "__main__":
|
||||
get_user_order("fuyixuan0628-印刷vip客户服务群", "去看海吗")
|
||||
get_user_order("小旋honey-印刷vip客户服务群@喜印说", "宇宙大女神")
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
from .ctx import CTX_BG_TASKS
|
||||
|
||||
|
||||
class BgTasks:
|
||||
"""后台任务统一管理"""
|
||||
|
||||
@classmethod
|
||||
async def init_bg_tasks_obj(cls):
|
||||
"""实例化后台任务,并设置到上下文"""
|
||||
bg_tasks = BackgroundTasks()
|
||||
CTX_BG_TASKS.set(bg_tasks)
|
||||
|
||||
@classmethod
|
||||
async def get_bg_tasks_obj(cls):
|
||||
"""从上下文中获取后台任务实例"""
|
||||
return CTX_BG_TASKS.get()
|
||||
|
||||
@classmethod
|
||||
async def add_task(cls, func, *args, **kwargs):
|
||||
"""添加后台任务"""
|
||||
bg_tasks = await cls.get_bg_tasks_obj()
|
||||
bg_tasks.add_task(func, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
async def execute_tasks(cls):
|
||||
"""执行后台任务,一般是请求结果返回之后执行"""
|
||||
bg_tasks = await cls.get_bg_tasks_obj()
|
||||
if bg_tasks.tasks:
|
||||
await bg_tasks()
|
||||
@@ -0,0 +1,163 @@
|
||||
# core/cache.py
|
||||
import json as _stdlib_json
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any, AsyncGenerator, Optional, Union
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from redis.asyncio import Redis
|
||||
|
||||
# ===== 尝试使用你的 fff,否则回退到 stdlib =====
|
||||
try:
|
||||
import orjson as json
|
||||
except ImportError:
|
||||
json = _stdlib_json # type: ignore
|
||||
|
||||
|
||||
# ===== Redis 客户端单例(可替换为你自己的)=====
|
||||
class RedisClient:
|
||||
_instance: Optional["RedisClient"] = None
|
||||
_redis: Optional[Redis] = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
async def init_redis(self, url: str = "redis://localhost:6379/0") -> None:
|
||||
if self._redis is None:
|
||||
self._redis = Redis.from_url(url, decode_responses=False)
|
||||
|
||||
@property
|
||||
def client(self) -> Redis:
|
||||
if self._redis is None:
|
||||
raise RuntimeError("Redis not initialized. Call init_redis() first.")
|
||||
return self._redis
|
||||
|
||||
async def close(self) -> None:
|
||||
if self._redis:
|
||||
await self._redis.close()
|
||||
self._redis = None
|
||||
|
||||
|
||||
redis_client = RedisClient()
|
||||
|
||||
|
||||
# ===== FastAPI 依赖注入 =====
|
||||
async def get_redis() -> Redis:
|
||||
"""FastAPI 依赖:获取 Redis 客户端"""
|
||||
return redis_client.client
|
||||
|
||||
async def invalidate_cache(key: str) -> bool:
|
||||
"""
|
||||
主动删除缓存键。
|
||||
返回是否成功删除(Redis delete 返回被删除的 key 数量)。
|
||||
"""
|
||||
try:
|
||||
result = await redis_client.client.delete(key)
|
||||
return result > 0
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to invalidate cache key: {key}", exc_info=e)
|
||||
return False
|
||||
|
||||
# ===== 缓存上下文管理器 =====
|
||||
class CacheResult:
|
||||
__slots__ = ("key", "hit", "value", "_to_set", "_set_called")
|
||||
|
||||
def __init__(self, key: str) -> None:
|
||||
self.key = key
|
||||
self.hit = False
|
||||
self.value: Any = None
|
||||
self._to_set: Any = None
|
||||
self._set_called = False
|
||||
|
||||
def set(self, value: Any) -> None:
|
||||
"""标记要缓存的值(可为 None)"""
|
||||
self._to_set = value
|
||||
self._set_called = True
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def cache_if(
|
||||
key: str,
|
||||
ttl: int = 3600,
|
||||
redis: Optional[Redis] = None,
|
||||
) -> AsyncGenerator[CacheResult, None]:
|
||||
"""
|
||||
异步缓存上下文管理器,支持 None 值缓存(防穿透)。
|
||||
|
||||
Args:
|
||||
key: 缓存键
|
||||
ttl: 正常值缓存时间(秒)
|
||||
redis: 可选 Redis 客户端(用于测试或自定义)
|
||||
|
||||
Usage:
|
||||
async with cache_if("report:123") as cache:
|
||||
if cache.hit:
|
||||
return cache.value
|
||||
result = await compute()
|
||||
cache.set(result) # result 可为 None
|
||||
"""
|
||||
result = CacheResult(key)
|
||||
client = redis or redis_client.client
|
||||
|
||||
# 尝试读缓存
|
||||
try:
|
||||
cached_val = await client.get(key)
|
||||
if cached_val is not None:
|
||||
# 解码
|
||||
if cached_val == b"__NULL__":
|
||||
result.hit = True
|
||||
result.value = None
|
||||
else:
|
||||
result.hit = True
|
||||
result.value = json.loads(cached_val)
|
||||
except Exception:
|
||||
# Redis 不可用,降级(不中断主流程)
|
||||
logger.exception(f"Redis get error for key: {key}")
|
||||
pass
|
||||
|
||||
yield result
|
||||
|
||||
# 写缓存(仅当调用了 set())
|
||||
if result._set_called:
|
||||
try:
|
||||
if result._to_set is None:
|
||||
val = b"__NULL__"
|
||||
ex = 60 # 空值短 TTL
|
||||
else:
|
||||
# 注意:orjson.dumps 返回 bytes,stdlib 返回 str → 统一转 bytes
|
||||
serialized = json.dumps(result._to_set)
|
||||
val = serialized if isinstance(serialized, bytes) else serialized.encode("utf-8")
|
||||
ex = ttl
|
||||
await client.setex(key, ex, val)
|
||||
except Exception:
|
||||
# 写缓存失败,不影响主流程
|
||||
pass
|
||||
|
||||
|
||||
# ===== 装饰器版(可选补充)=====
|
||||
from functools import wraps
|
||||
import asyncio
|
||||
import hashlib
|
||||
|
||||
def cached(ttl: int = 3600):
|
||||
"""函数缓存装饰器(使用 cache_if)"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
# 生成 key(简单版,可替换为更 robust 的)
|
||||
key_data = str(args) + str(sorted(kwargs.items()))
|
||||
key = f"cached:{func.__name__}:{hashlib.md5(key_data.encode()).hexdigest()}"
|
||||
logger.debug(f"Cache key: {key}")
|
||||
|
||||
async with cache_if(key, ttl) as cache:
|
||||
if cache.hit:
|
||||
logger.debug(f"Cache hit for key: {key}")
|
||||
return cache.value
|
||||
result = await func(*args, **kwargs)
|
||||
cache.set(result)
|
||||
return result
|
||||
return wrapper
|
||||
return decorator
|
||||
@@ -0,0 +1,78 @@
|
||||
from typing import Any, Callable, Dict, Generic, List, NewType, Tuple, Type, TypeVar, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
from tortoise.expressions import Q
|
||||
from tortoise.models import Model
|
||||
|
||||
Total = NewType("Total", int)
|
||||
ModelType = TypeVar("ModelType", bound=Model)
|
||||
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
|
||||
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
|
||||
|
||||
|
||||
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
||||
def __init__(self, model: Type[ModelType]):
|
||||
self.model = model
|
||||
|
||||
async def is_exist(self, **kwargs) -> bool:
|
||||
return await self.model.filter(**kwargs).first()
|
||||
|
||||
async def all(self, search: Q = Q()) -> List[ModelType]:
|
||||
return await self.model.filter(search).all()
|
||||
|
||||
async def get(self, id: int) -> ModelType:
|
||||
return await self.model.get(id=id)
|
||||
|
||||
async def list(self, page: int, page_size: int, search: Q = Q(), order: list = []) -> Tuple[Total, List[ModelType]]:
|
||||
query = self.model.filter(search)
|
||||
return await query.count(), await query.offset((page - 1) * page_size).limit(page_size).order_by(*order)
|
||||
|
||||
async def create(self, obj_in: CreateSchemaType) -> ModelType:
|
||||
if isinstance(obj_in, Dict):
|
||||
obj_dict = obj_in
|
||||
else:
|
||||
obj_dict = obj_in.model_dump()
|
||||
obj = self.model(**obj_dict)
|
||||
await obj.save()
|
||||
return obj
|
||||
|
||||
async def copy(self, id: int, handler: Callable[[Dict[str, Any]], Dict[str, Any]] = None) -> ModelType:
|
||||
obj = await self.get(id=id)
|
||||
obj_dict = await obj.to_dict()
|
||||
drop_keys = ["id", "created_at", "updated_at"]
|
||||
for key in drop_keys:
|
||||
obj_dict.pop(key, None)
|
||||
if handler:
|
||||
obj_dict = handler(obj_dict)
|
||||
obj = self.model(**obj_dict)
|
||||
await obj.save()
|
||||
return obj
|
||||
|
||||
async def update(self, id: int, obj_in: Union[UpdateSchemaType, Dict[str, Any]]) -> ModelType:
|
||||
if isinstance(obj_in, Dict):
|
||||
obj_dict = obj_in
|
||||
else:
|
||||
obj_dict = obj_in.model_dump(exclude_unset=True, exclude={"id"})
|
||||
obj = await self.get(id=id)
|
||||
obj = obj.update_from_dict(obj_dict)
|
||||
await obj.save()
|
||||
return obj
|
||||
|
||||
async def create_or_update(self, obj_in: CreateSchemaType, query_kwargs: Dict[str, Any], update_kwargs: Dict[str, Any] = None) -> ModelType:
|
||||
update_kwargs = update_kwargs or {}
|
||||
orm = await self.model.filter(**query_kwargs).first()
|
||||
if orm:
|
||||
need_update = False if update_kwargs else True
|
||||
for key, value in update_kwargs.items():
|
||||
if getattr(orm, key) != value:
|
||||
need_update = True
|
||||
break
|
||||
if need_update:
|
||||
return 'update', await self.update(orm.id, obj_in)
|
||||
else:
|
||||
return False, orm
|
||||
return 'create', await self.create(obj_in)
|
||||
|
||||
async def remove(self, id: int) -> None:
|
||||
obj = await self.get(id=id)
|
||||
await obj.delete()
|
||||
@@ -0,0 +1,17 @@
|
||||
import contextvars
|
||||
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
CTX_USER_ID: contextvars.ContextVar[int] = contextvars.ContextVar("user_id", default=0)
|
||||
CTX_USER_NAME: contextvars.ContextVar[str] = contextvars.ContextVar("user_name", default="")
|
||||
CTX_BG_TASKS: contextvars.ContextVar[BackgroundTasks] = contextvars.ContextVar("bg_task", default=None)
|
||||
|
||||
def set_ctx_weixin_user(user_id: int, user_name: str):
|
||||
CTX_USER_ID.set(user_id)
|
||||
CTX_USER_NAME.set(user_name)
|
||||
|
||||
def get_ctx_weixin_user():
|
||||
return {
|
||||
"user_id": CTX_USER_ID.get(),
|
||||
"user_name": CTX_USER_NAME.get(),
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
from typing import Optional
|
||||
|
||||
import jwt
|
||||
from fastapi import Depends, Header, HTTPException, Request
|
||||
|
||||
from app.core.ctx import CTX_USER_ID
|
||||
from app.models import Role, User, WeixinUser
|
||||
from app.settings import settings
|
||||
|
||||
|
||||
class AuthControl:
|
||||
@classmethod
|
||||
async def is_authed(cls, token: str = Header(..., description="token验证")) -> Optional["User"]:
|
||||
try:
|
||||
if token == "dev":
|
||||
user = await User.filter().first()
|
||||
user_id = user.id
|
||||
else:
|
||||
decode_data = jwt.decode(token, settings.SECRET_KEY, algorithms=settings.JWT_ALGORITHM)
|
||||
user_id = decode_data.get("user_id")
|
||||
user = await User.filter(id=user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="Authentication failed")
|
||||
CTX_USER_ID.set(int(user_id))
|
||||
return user
|
||||
except jwt.DecodeError:
|
||||
raise HTTPException(status_code=401, detail="无效的Token")
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(status_code=401, detail="登录已过期")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"{repr(e)}")
|
||||
|
||||
@classmethod
|
||||
async def weixin_user(cls, request: Request) -> WeixinUser:
|
||||
path = request.url.path
|
||||
if path in [
|
||||
"/api/v1/msg/new_order",
|
||||
"/api/v1/event/feishu",
|
||||
"/api/v1/msg/get_order_user_days_before",
|
||||
]:
|
||||
return None
|
||||
|
||||
token = request.headers.get("token")
|
||||
user = await WeixinUser.filter(userid=token).first()
|
||||
assert user, "Authentication failed"
|
||||
CTX_USER_ID.set(int(user.id))
|
||||
return user # 返回的是 WeixinUser 实例
|
||||
|
||||
class PermissionControl:
|
||||
@classmethod
|
||||
async def has_permission(cls, request: Request, current_user: User = Depends(AuthControl.is_authed)) -> None:
|
||||
if current_user.is_superuser:
|
||||
return
|
||||
method = request.method
|
||||
path = request.url.path
|
||||
roles: list[Role] = await current_user.roles
|
||||
if not roles:
|
||||
raise HTTPException(status_code=403, detail="The user is not bound to a role")
|
||||
apis = [await role.apis for role in roles]
|
||||
permission_apis = list(set((api.method, api.path) for api in sum(apis, [])))
|
||||
# path = "/api/v1/auth/userinfo"
|
||||
# method = "GET"
|
||||
if (method, path) not in permission_apis:
|
||||
raise HTTPException(status_code=403, detail=f"Permission denied method:{method} path:{path}")
|
||||
|
||||
|
||||
DependAuth = Depends(AuthControl.is_authed)
|
||||
DependWeixinUser = Depends(AuthControl.weixin_user)
|
||||
DependPermisson = Depends(PermissionControl.has_permission)
|
||||
@@ -0,0 +1,150 @@
|
||||
from fastapi.exceptions import (
|
||||
HTTPException,
|
||||
RequestValidationError,
|
||||
ResponseValidationError,
|
||||
)
|
||||
from fastapi.requests import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from tortoise.exceptions import DoesNotExist, IntegrityError
|
||||
|
||||
from app.http_base import HttpResp
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SettingNotFound(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AssertException(Exception):
|
||||
"""断言异常"""
|
||||
def __init__(self, message: str, code: int = 1001):
|
||||
self.message = message
|
||||
self.code = code
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
async def DoesNotExistHandle(req: Request, exc: DoesNotExist) -> JSONResponse:
|
||||
"""处理对象不存在异常"""
|
||||
resp = HttpResp.REQUEST_404_ERROR
|
||||
content = dict(
|
||||
code=resp.code,
|
||||
msg=f"Object has not found, exc: {exc}, query_params: {req.query_params}",
|
||||
data=None
|
||||
)
|
||||
logger.warning(f"Object not found: {exc}, path: {req.url.path}")
|
||||
return JSONResponse(content=content, status_code=200)
|
||||
|
||||
|
||||
async def IntegrityHandle(req: Request, exc: IntegrityError) -> JSONResponse:
|
||||
"""处理数据完整性异常"""
|
||||
resp = HttpResp.SYSTEM_ERROR
|
||||
content = dict(
|
||||
code=resp.code,
|
||||
msg=f"IntegrityError: {str(exc)}",
|
||||
data=None
|
||||
)
|
||||
logger.error(f"Integrity error: {exc}, path: {req.url.path}", exc_info=True)
|
||||
return JSONResponse(content=content, status_code=200)
|
||||
|
||||
|
||||
async def HttpExcHandle(req: Request, exc: HTTPException) -> JSONResponse:
|
||||
"""处理 HTTP 异常"""
|
||||
content = dict(
|
||||
code=exc.status_code,
|
||||
msg=exc.detail,
|
||||
data=None
|
||||
)
|
||||
logger.warning(f"HTTP exception: {exc.status_code} - {exc.detail}, path: {req.url.path}")
|
||||
return JSONResponse(content=content, status_code=200)
|
||||
|
||||
|
||||
async def RequestValidationHandle(req: Request, exc: RequestValidationError) -> JSONResponse:
|
||||
"""处理请求验证异常"""
|
||||
resp = HttpResp.PARAMS_VALID_ERROR
|
||||
errors = []
|
||||
for error in exc.errors():
|
||||
errors.append({
|
||||
"loc": error["loc"],
|
||||
"msg": error["msg"],
|
||||
"type": error["type"]
|
||||
})
|
||||
|
||||
content = dict(
|
||||
code=resp.code,
|
||||
msg=f"{resp.msg}, errors: {errors}",
|
||||
data=errors
|
||||
)
|
||||
logger.warning(f"Request validation error: {errors}, path: {req.url.path}")
|
||||
return JSONResponse(content=content, status_code=200)
|
||||
|
||||
|
||||
async def ResponseValidationHandle(req: Request, exc: ResponseValidationError) -> JSONResponse:
|
||||
"""处理响应验证异常"""
|
||||
resp = HttpResp.SYSTEM_ERROR
|
||||
content = dict(
|
||||
code=resp.code,
|
||||
msg=f"Response validation error: {str(exc)}",
|
||||
data=None
|
||||
)
|
||||
logger.error(f"Response validation error: {exc}, path: {req.url.path}", exc_info=True)
|
||||
return JSONResponse(content=content, status_code=200)
|
||||
|
||||
|
||||
async def AssertExceptionHandle(request: Request, exc: AssertException) -> JSONResponse:
|
||||
"""处理断言异常"""
|
||||
resp = HttpResp.ASSERT_ARGUMENT_ERROR
|
||||
content = dict(
|
||||
code=exc.code,
|
||||
msg=exc.message,
|
||||
data=None
|
||||
)
|
||||
logger.warning(f"Assert exception: {exc.message}, path: {request.url.path}")
|
||||
return JSONResponse(
|
||||
content=content,
|
||||
status_code=200
|
||||
)
|
||||
|
||||
|
||||
async def AssertionErrorHandle(request: Request, exc: AssertionError) -> JSONResponse:
|
||||
"""处理 Python assert 异常"""
|
||||
resp = HttpResp.ASSERT_ARGUMENT_ERROR
|
||||
message = str(exc) if exc.args else resp.msg
|
||||
content = dict(
|
||||
code=resp.code,
|
||||
msg=message,
|
||||
data=None
|
||||
)
|
||||
logger.warning(f"Assertion error: {message}, path: {request.url.path}")
|
||||
return JSONResponse(
|
||||
content=content,
|
||||
status_code=200
|
||||
)
|
||||
|
||||
|
||||
async def GlobalExceptionHandler(request: Request, exc: Exception) -> JSONResponse:
|
||||
"""全局异常处理器"""
|
||||
resp = HttpResp.SYSTEM_ERROR
|
||||
content = dict(
|
||||
code=resp.code,
|
||||
msg=f"{resp.msg}: {str(exc)}",
|
||||
data=None
|
||||
)
|
||||
logger.error(f"Global exception: {str(exc)}, path: {request.url.path}", exc_info=True)
|
||||
return JSONResponse(
|
||||
content=content,
|
||||
status_code=200
|
||||
)
|
||||
|
||||
|
||||
# 便捷函数用于注册所有异常处理器
|
||||
def register_exception_handlers(app):
|
||||
"""注册所有异常处理器到 FastAPI 应用"""
|
||||
app.add_exception_handler(DoesNotExist, DoesNotExistHandle)
|
||||
app.add_exception_handler(IntegrityError, IntegrityHandle)
|
||||
app.add_exception_handler(HTTPException, HttpExcHandle)
|
||||
app.add_exception_handler(RequestValidationError, RequestValidationHandle)
|
||||
app.add_exception_handler(ResponseValidationError, ResponseValidationHandle)
|
||||
app.add_exception_handler(AssertException, AssertExceptionHandle)
|
||||
app.add_exception_handler(AssertionError, AssertionErrorHandle) # 捕获 Python assert
|
||||
app.add_exception_handler(Exception, GlobalExceptionHandler) # 全局异常处理器
|
||||
@@ -0,0 +1,273 @@
|
||||
import shutil
|
||||
|
||||
from aerich import Command
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware import Middleware
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from tortoise.expressions import Q
|
||||
|
||||
from app.api import api_router
|
||||
from app.controllers.api import api_controller
|
||||
from app.controllers.user import UserCreate, user_controller
|
||||
from app.core.exceptions import register_exception_handlers
|
||||
from app.log import logger
|
||||
from app.models.admin import Api, Menu, Role, Dept
|
||||
from app.models.automation import Task, Scenario
|
||||
from app.schemas.menus import MenuType
|
||||
from app.settings.config import settings
|
||||
from app.core.cache import redis_client
|
||||
|
||||
from .middlewares import BackGroundTaskMiddleware, HttpAuditLogMiddleware
|
||||
|
||||
|
||||
def make_middlewares():
|
||||
middleware = [
|
||||
Middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.CORS_ORIGINS,
|
||||
allow_credentials=settings.CORS_ALLOW_CREDENTIALS,
|
||||
allow_methods=settings.CORS_ALLOW_METHODS,
|
||||
allow_headers=settings.CORS_ALLOW_HEADERS,
|
||||
),
|
||||
Middleware(BackGroundTaskMiddleware),
|
||||
Middleware(
|
||||
HttpAuditLogMiddleware,
|
||||
methods=["GET", "POST", "PUT", "DELETE"],
|
||||
exclude_paths=[
|
||||
"/api/v1/msg/new_order",
|
||||
"/api/v1/weixin/jssdk-config",
|
||||
"/api/v1/base/access_token",
|
||||
"/docs",
|
||||
"/api-docs",
|
||||
"/openapi.json",
|
||||
"/static/*",
|
||||
"/gen/*",
|
||||
],
|
||||
),
|
||||
]
|
||||
return middleware
|
||||
|
||||
def mount_static_and_config_swagger(app: FastAPI):
|
||||
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.openapi.docs import get_swagger_ui_html
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="静态文件")
|
||||
|
||||
@app.get(app.docs_url, include_in_schema=False)
|
||||
async def custom_swagger_ui_html():
|
||||
print('访问文档')
|
||||
return get_swagger_ui_html(
|
||||
openapi_url=app.openapi_url,
|
||||
title=app.title,
|
||||
swagger_js_url="/static/swagger-ui/swagger-ui-bundle.js",
|
||||
swagger_css_url="/static/swagger-ui/swagger-ui.css"
|
||||
)
|
||||
|
||||
def register_exceptions(app: FastAPI):
|
||||
register_exception_handlers(app)
|
||||
print('注册异常处理完成')
|
||||
|
||||
|
||||
def register_routers(app: FastAPI, prefix: str = "/api"):
|
||||
app.include_router(api_router, prefix=prefix)
|
||||
|
||||
|
||||
async def init_superuser():
|
||||
user = await user_controller.model.exists()
|
||||
if not user:
|
||||
await user_controller.create_user(
|
||||
UserCreate(
|
||||
username="admin",
|
||||
email="admin@admin.com",
|
||||
password="123456",
|
||||
is_active=True,
|
||||
is_superuser=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def init_menus():
|
||||
menus = await Menu.exists()
|
||||
if not menus:
|
||||
parent_menu = await Menu.create(
|
||||
menu_type=MenuType.CATALOG,
|
||||
name="系统管理",
|
||||
path="/system",
|
||||
order=1,
|
||||
parent_id=0,
|
||||
icon="carbon:gui-management",
|
||||
is_hidden=False,
|
||||
component="Layout",
|
||||
keepalive=False,
|
||||
redirect="/system/user",
|
||||
)
|
||||
children_menu = [
|
||||
Menu(
|
||||
menu_type=MenuType.MENU,
|
||||
name="用户管理",
|
||||
path="user",
|
||||
order=1,
|
||||
parent_id=parent_menu.id,
|
||||
icon="material-symbols:person-outline-rounded",
|
||||
is_hidden=False,
|
||||
component="/system/user",
|
||||
keepalive=False,
|
||||
),
|
||||
Menu(
|
||||
menu_type=MenuType.MENU,
|
||||
name="角色管理",
|
||||
path="role",
|
||||
order=2,
|
||||
parent_id=parent_menu.id,
|
||||
icon="carbon:user-role",
|
||||
is_hidden=False,
|
||||
component="/system/role",
|
||||
keepalive=False,
|
||||
),
|
||||
Menu(
|
||||
menu_type=MenuType.MENU,
|
||||
name="菜单管理",
|
||||
path="menu",
|
||||
order=3,
|
||||
parent_id=parent_menu.id,
|
||||
icon="material-symbols:list-alt-outline",
|
||||
is_hidden=False,
|
||||
component="/system/menu",
|
||||
keepalive=False,
|
||||
),
|
||||
Menu(
|
||||
menu_type=MenuType.MENU,
|
||||
name="API管理",
|
||||
path="api",
|
||||
order=4,
|
||||
parent_id=parent_menu.id,
|
||||
icon="ant-design:api-outlined",
|
||||
is_hidden=False,
|
||||
component="/system/api",
|
||||
keepalive=False,
|
||||
),
|
||||
Menu(
|
||||
menu_type=MenuType.MENU,
|
||||
name="部门管理",
|
||||
path="dept",
|
||||
order=5,
|
||||
parent_id=parent_menu.id,
|
||||
icon="mingcute:department-line",
|
||||
is_hidden=False,
|
||||
component="/system/dept",
|
||||
keepalive=False,
|
||||
),
|
||||
Menu(
|
||||
menu_type=MenuType.MENU,
|
||||
name="审计日志",
|
||||
path="auditlog",
|
||||
order=6,
|
||||
parent_id=parent_menu.id,
|
||||
icon="ph:clipboard-text-bold",
|
||||
is_hidden=False,
|
||||
component="/system/auditlog",
|
||||
keepalive=False,
|
||||
),
|
||||
]
|
||||
await Menu.bulk_create(children_menu)
|
||||
await Menu.create(
|
||||
menu_type=MenuType.MENU,
|
||||
name="一级菜单",
|
||||
path="/top-menu",
|
||||
order=2,
|
||||
parent_id=0,
|
||||
icon="material-symbols:featured-play-list-outline",
|
||||
is_hidden=False,
|
||||
component="/top-menu",
|
||||
keepalive=False,
|
||||
redirect="",
|
||||
)
|
||||
|
||||
|
||||
async def init_apis():
|
||||
apis = await api_controller.model.exists()
|
||||
if not apis:
|
||||
await api_controller.refresh_api()
|
||||
|
||||
|
||||
async def init_db():
|
||||
command = Command(tortoise_config=settings.TORTOISE_ORM)
|
||||
try:
|
||||
await command.init_db(safe=True)
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
await command.init()
|
||||
try:
|
||||
await command.migrate()
|
||||
except AttributeError:
|
||||
logger.warning("unable to retrieve model history from database, model history will be created from scratch")
|
||||
shutil.rmtree("migrations")
|
||||
await command.init_db(safe=True)
|
||||
|
||||
await command.upgrade(run_in_transaction=True)
|
||||
|
||||
|
||||
async def init_roles():
|
||||
roles = await Role.exists()
|
||||
if not roles:
|
||||
admin_role = await Role.create(
|
||||
name="管理员",
|
||||
desc="管理员角色",
|
||||
)
|
||||
user_role = await Role.create(
|
||||
name="普通用户",
|
||||
desc="普通用户角色",
|
||||
)
|
||||
|
||||
await Dept.create(name="默认部门")
|
||||
|
||||
# 分配所有API给管理员角色
|
||||
all_apis = await Api.all()
|
||||
await admin_role.apis.add(*all_apis)
|
||||
# 分配所有菜单给管理员和普通用户
|
||||
all_menus = await Menu.all()
|
||||
await admin_role.menus.add(*all_menus)
|
||||
await user_role.menus.add(*all_menus)
|
||||
|
||||
# 为普通用户分配基本API
|
||||
basic_apis = await Api.filter(Q(method__in=["GET"]) | Q(tags="基础模块"))
|
||||
await user_role.apis.add(*basic_apis)
|
||||
|
||||
async def init_cache():
|
||||
await redis_client.init_redis(settings.REDIS_URL)
|
||||
|
||||
async def init_task():
|
||||
|
||||
task = await Task.exists(title="系统待办")
|
||||
if task: return
|
||||
|
||||
scenario = await Scenario.create(
|
||||
title="系统待办",
|
||||
visible=False,
|
||||
trigger={},
|
||||
actions=[],
|
||||
enabled=False,
|
||||
notes="用于为所有存量数据创建的待办任务",
|
||||
)
|
||||
task = await Task.create(
|
||||
title="系统待办",
|
||||
status='success',
|
||||
ui_schema={},
|
||||
source_scenario=scenario,
|
||||
notes="当系统有新的待办任务时关联动作",
|
||||
)
|
||||
|
||||
|
||||
async def init_data():
|
||||
await init_db()
|
||||
await init_task()
|
||||
await init_cache()
|
||||
await init_superuser()
|
||||
await init_menus()
|
||||
await init_apis()
|
||||
await init_roles()
|
||||
|
||||
async def tear_down():
|
||||
await redis_client.close()
|
||||
@@ -0,0 +1,192 @@
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Any, AsyncGenerator
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import Response
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||
from starlette.requests import Request
|
||||
from starlette.types import ASGIApp, Receive, Scope, Send
|
||||
|
||||
from app.core.dependency import AuthControl
|
||||
from app.models.admin import AuditLog, User
|
||||
|
||||
from .bgtask import BgTasks
|
||||
|
||||
|
||||
class SimpleBaseMiddleware:
|
||||
def __init__(self, app: ASGIApp) -> None:
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
if scope["type"] != "http":
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
request = Request(scope, receive=receive)
|
||||
|
||||
response = await self.before_request(request) or self.app
|
||||
await response(request.scope, request.receive, send)
|
||||
await self.after_request(request)
|
||||
|
||||
async def before_request(self, request: Request):
|
||||
return self.app
|
||||
|
||||
async def after_request(self, request: Request):
|
||||
return None
|
||||
|
||||
|
||||
class BackGroundTaskMiddleware(SimpleBaseMiddleware):
|
||||
async def before_request(self, request):
|
||||
await BgTasks.init_bg_tasks_obj()
|
||||
|
||||
async def after_request(self, request):
|
||||
await BgTasks.execute_tasks()
|
||||
|
||||
|
||||
class HttpAuditLogMiddleware(BaseHTTPMiddleware):
|
||||
def __init__(self, app, methods: list[str], exclude_paths: list[str]):
|
||||
super().__init__(app)
|
||||
self.methods = methods
|
||||
self.exclude_paths = exclude_paths
|
||||
self.audit_log_paths = ["/api/v1/auditlog/list"]
|
||||
self.max_body_size = 1024 * 1024 # 1MB 响应体大小限制
|
||||
|
||||
async def get_request_args(self, request: Request) -> dict:
|
||||
args = {}
|
||||
# 获取查询参数
|
||||
for key, value in request.query_params.items():
|
||||
args[key] = value
|
||||
|
||||
# 判断是否为文件上传请求
|
||||
content_type = request.headers.get("content-type", "").lower()
|
||||
is_upload = (
|
||||
request.method in {"POST", "PUT"} # 上传通常用 POST/PUT
|
||||
and (
|
||||
"multipart/form-data" in content_type # 标准文件上传
|
||||
or "application/octet-stream" in content_type # 二进制流上传(较少见)
|
||||
)
|
||||
)
|
||||
|
||||
if is_upload:
|
||||
return {}
|
||||
|
||||
# 获取请求体
|
||||
if request.method in ["POST", "PUT", "PATCH"]:
|
||||
try:
|
||||
body = await request.json()
|
||||
args.update(body)
|
||||
except json.JSONDecodeError:
|
||||
try:
|
||||
body = await request.form()
|
||||
args.update(body)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return args
|
||||
|
||||
async def get_response_body(self, request: Request, response: Response) -> Any:
|
||||
content_type = response.headers.get("content-type", "").lower()
|
||||
if not content_type.startswith("application/json"):
|
||||
return {"msg": "Non-JSON response (e.g., file download), skipped for audit"}
|
||||
|
||||
# 检查Content-Length
|
||||
content_length = response.headers.get("content-length")
|
||||
if content_length and int(content_length) > self.max_body_size:
|
||||
return {"code": 0, "msg": "Response too large to log", "data": None}
|
||||
|
||||
if hasattr(response, "body"):
|
||||
body = response.body
|
||||
else:
|
||||
body_chunks = []
|
||||
async for chunk in response.body_iterator:
|
||||
if not isinstance(chunk, bytes):
|
||||
chunk = chunk.encode(response.charset)
|
||||
body_chunks.append(chunk)
|
||||
|
||||
response.body_iterator = self._async_iter(body_chunks)
|
||||
body = b"".join(body_chunks)
|
||||
|
||||
if any(request.url.path.startswith(path) for path in self.audit_log_paths):
|
||||
try:
|
||||
data = self.lenient_json(body)
|
||||
# 只保留基本信息,去除详细的响应内容
|
||||
if isinstance(data, dict):
|
||||
data.pop("response_body", None)
|
||||
if "data" in data and isinstance(data["data"], list):
|
||||
for item in data["data"]:
|
||||
item.pop("response_body", None)
|
||||
return data
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
return self.lenient_json(body)
|
||||
|
||||
def lenient_json(self, v: Any) -> Any:
|
||||
if isinstance(v, (str, bytes)):
|
||||
try:
|
||||
return json.loads(v)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return v
|
||||
|
||||
async def _async_iter(self, items: list[bytes]) -> AsyncGenerator[bytes, None]:
|
||||
for item in items:
|
||||
yield item
|
||||
|
||||
async def get_request_log(self, request: Request, response: Response) -> dict:
|
||||
"""
|
||||
根据request和response对象获取对应的日志记录数据
|
||||
"""
|
||||
data: dict = {"path": request.url.path, "status": response.status_code, "method": request.method}
|
||||
# 路由信息
|
||||
app: FastAPI = request.app
|
||||
for route in app.routes:
|
||||
if (
|
||||
isinstance(route, APIRoute)
|
||||
and route.path_regex.match(request.url.path)
|
||||
and request.method in route.methods
|
||||
):
|
||||
data["module"] = ",".join(route.tags)
|
||||
data["summary"] = route.summary
|
||||
# 获取用户信息
|
||||
try:
|
||||
token = request.headers.get("token")
|
||||
user_obj = None
|
||||
if token:
|
||||
user_obj: User = await AuthControl.is_authed(token)
|
||||
data["user_id"] = user_obj.id if user_obj else 0
|
||||
data["username"] = user_obj.username if user_obj else ""
|
||||
except Exception:
|
||||
data["user_id"] = 0
|
||||
data["username"] = ""
|
||||
return data
|
||||
|
||||
async def before_request(self, request: Request):
|
||||
request_args = await self.get_request_args(request)
|
||||
request.state.request_args = request_args
|
||||
|
||||
async def after_request(self, request: Request, response: Response, process_time: int):
|
||||
if request.method in self.methods:
|
||||
for path in self.exclude_paths:
|
||||
if re.search(path, request.url.path, re.I) is not None:
|
||||
return
|
||||
data: dict = await self.get_request_log(request=request, response=response)
|
||||
data["response_time"] = process_time
|
||||
|
||||
data["request_args"] = request.state.request_args
|
||||
data["response_body"] = await self.get_response_body(request, response)
|
||||
await AuditLog.create(**data)
|
||||
|
||||
return response
|
||||
|
||||
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
||||
start_time: datetime = datetime.now()
|
||||
await self.before_request(request)
|
||||
response = await call_next(request)
|
||||
end_time: datetime = datetime.now()
|
||||
process_time = int((end_time.timestamp() - start_time.timestamp()) * 1000)
|
||||
await self.after_request(request, response, process_time)
|
||||
return response
|
||||
@@ -0,0 +1,66 @@
|
||||
import inspect
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from typing import Callable, TypeVar
|
||||
|
||||
import pytz
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.settings import settings
|
||||
|
||||
__all__ = ['HttpCode', 'HttpResp', 'unified_resp']
|
||||
|
||||
RT = TypeVar('RT') # 返回类型
|
||||
HttpCode = namedtuple('HttpResp', ['code', 'msg'])
|
||||
|
||||
|
||||
class HttpResp:
|
||||
"""HTTP响应结果
|
||||
"""
|
||||
SUCCESS = HttpCode(200, '成功')
|
||||
FAILED = HttpCode(300, '失败')
|
||||
PARAMS_VALID_ERROR = HttpCode(310, '参数校验错误')
|
||||
PARAMS_TYPE_ERROR = HttpCode(311, '参数类型错误')
|
||||
REQUEST_METHOD_ERROR = HttpCode(312, '请求方法错误')
|
||||
ASSERT_ARGUMENT_ERROR = HttpCode(313, '断言参数错误')
|
||||
|
||||
LOGIN_ACCOUNT_ERROR = HttpCode(330, '登录账号或密码错误')
|
||||
LOGIN_DISABLE_ERROR = HttpCode(331, '登录账号已被禁用了')
|
||||
TOKEN_EMPTY = HttpCode(332, 'token参数为空')
|
||||
TOKEN_INVALID = HttpCode(333, 'token参数无效')
|
||||
|
||||
NO_PERMISSION = HttpCode(403, '无相关权限')
|
||||
REQUEST_404_ERROR = HttpCode(404, '请求接口不存在')
|
||||
|
||||
SYSTEM_ERROR = HttpCode(500, '系统错误')
|
||||
SYSTEM_TIMEOUT_ERROR = HttpCode(504, '请求超时')
|
||||
|
||||
# 时区
|
||||
timezone = pytz.timezone('Asia/Shanghai')
|
||||
|
||||
def unified_resp(func: Callable[..., RT]) -> Callable[..., RT]:
|
||||
"""统一响应格式
|
||||
接口正常返回时,统一响应结果格式
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs) -> RT:
|
||||
if inspect.iscoroutinefunction(func):
|
||||
resp = await func(*args, **kwargs) or []
|
||||
else:
|
||||
resp = func(*args, **kwargs) or []
|
||||
return JSONResponse(
|
||||
content=jsonable_encoder(
|
||||
# 正常请求响应
|
||||
{'code': HttpResp.SUCCESS.code, 'msg': HttpResp.SUCCESS.msg, 'data': resp},
|
||||
by_alias=False,
|
||||
# 自定义日期时间格式编码器
|
||||
custom_encoder={
|
||||
datetime: lambda dt: dt.replace(tzinfo=pytz.utc).astimezone(timezone)
|
||||
.strftime(settings.DATETIME_FORMAT)}),
|
||||
media_type='application/json;charset=utf-8'
|
||||
)
|
||||
|
||||
return wrapper
|
||||
@@ -0,0 +1 @@
|
||||
from .log import logger as logger
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
from app.settings import settings
|
||||
import logging
|
||||
import logging.handlers
|
||||
from pathlib import Path
|
||||
|
||||
class Loggin:
|
||||
def __init__(self) -> None:
|
||||
debug = settings.DEBUG
|
||||
if debug:
|
||||
self.level = "DEBUG"
|
||||
else:
|
||||
self.level = "INFO"
|
||||
|
||||
def setup_logger(self):
|
||||
"""根据settings配置设置日志系统,控制台输出支持颜色"""
|
||||
# 获取日志配置
|
||||
log_level = self.level
|
||||
log_format = settings.LOG_FORMAT
|
||||
|
||||
# 获取文件日志配置
|
||||
file_logging_enabled = True
|
||||
log_file_path = ''
|
||||
|
||||
# 创建日志目录
|
||||
if file_logging_enabled:
|
||||
log_file_path = Path(settings.LOGS_ROOT) / "app.log"
|
||||
log_file_path.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
# 构建处理器列表
|
||||
handlers = []
|
||||
|
||||
# 控制台处理器(支持彩色输出)
|
||||
console_enabled = self.level == "DEBUG"
|
||||
if console_enabled:
|
||||
print('添加控制台日志打印')
|
||||
import colorlog # 新增:导入colorlog库
|
||||
|
||||
# 定义彩色日志级别对应的颜色
|
||||
log_colors = {
|
||||
'DEBUG': 'white',
|
||||
'INFO': 'green',
|
||||
'WARNING': 'yellow',
|
||||
'ERROR': 'red',
|
||||
'CRITICAL': 'bold_red',
|
||||
}
|
||||
|
||||
# 创建彩色格式化器
|
||||
console_formatter = colorlog.ColoredFormatter(
|
||||
# 彩色格式中需要使用%(log_color)s作为颜色标记
|
||||
"%(log_color)s" + log_format,
|
||||
datefmt=settings.DATETIME_FORMAT,
|
||||
log_colors=log_colors,
|
||||
reset=True,
|
||||
style='%'
|
||||
)
|
||||
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(console_formatter)
|
||||
handlers.append(console_handler)
|
||||
|
||||
# 文件处理器(如果启用)
|
||||
if file_logging_enabled:
|
||||
# 获取文件大小限制和备份数量
|
||||
max_bytes = settings.LOG_FILE_MAX_SIZE
|
||||
backup_count = settings.LOG_FILE_BACKUP_COUNT
|
||||
|
||||
# 文件日志使用普通格式化器(不添加颜色代码)
|
||||
file_formatter = logging.Formatter(
|
||||
log_format,
|
||||
datefmt=settings.DATETIME_FORMAT
|
||||
)
|
||||
|
||||
file_handler = logging.handlers.RotatingFileHandler(
|
||||
log_file_path,
|
||||
maxBytes=max_bytes,
|
||||
backupCount=backup_count,
|
||||
encoding='utf-8'
|
||||
)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
handlers.append(file_handler)
|
||||
|
||||
# 配置根日志记录器
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, log_level.upper(), logging.INFO),
|
||||
format=log_format,
|
||||
handlers=handlers,
|
||||
datefmt=settings.DATETIME_FORMAT
|
||||
)
|
||||
|
||||
# 设置第三方库的日志级别,减少噪音
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
logging.getLogger("selenium").setLevel(logging.WARNING)
|
||||
logging.getLogger("playwright").setLevel(logging.WARNING)
|
||||
logging.getLogger("tortoise").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
|
||||
# 记录日志系统初始化
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Logging configured - Level: {log_level}, File: {log_file_path if file_logging_enabled else 'Disabled'}")
|
||||
return logger
|
||||
|
||||
|
||||
|
||||
loggin = Loggin()
|
||||
logger = loggin.setup_logger()
|
||||
@@ -0,0 +1,5 @@
|
||||
# 新增model需要在这里导入
|
||||
from .admin import *
|
||||
from .automation import *
|
||||
from .weixin import *
|
||||
from .msg import *
|
||||
@@ -0,0 +1,297 @@
|
||||
from tortoise import fields
|
||||
from tortoise import fields as Fields
|
||||
|
||||
from app.schemas.menus import MenuType
|
||||
|
||||
from .base import BaseModel, TimestampMixin
|
||||
from .enums import MethodType
|
||||
|
||||
|
||||
class User(BaseModel, TimestampMixin):
|
||||
username = fields.CharField(max_length=20, unique=True, description="用户名称", index=True)
|
||||
alias = fields.CharField(max_length=30, null=True, description="姓名", index=True)
|
||||
email = fields.CharField(max_length=255, unique=True, description="邮箱", index=True)
|
||||
phone = fields.CharField(max_length=20, null=True, description="电话", index=True)
|
||||
password = fields.CharField(max_length=128, null=True, description="密码")
|
||||
is_active = fields.BooleanField(default=True, description="是否激活", index=True)
|
||||
is_superuser = fields.BooleanField(default=False, description="是否为超级管理员", index=True)
|
||||
last_login = fields.DatetimeField(null=True, description="最后登录时间", index=True)
|
||||
roles = fields.ManyToManyField("models.Role", related_name="user_roles")
|
||||
dept_id = fields.IntField(null=True, description="部门ID", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "user"
|
||||
|
||||
|
||||
class Role(BaseModel, TimestampMixin):
|
||||
name = fields.CharField(max_length=20, unique=True, description="角色名称", index=True)
|
||||
desc = fields.CharField(max_length=500, null=True, description="角色描述")
|
||||
menus = fields.ManyToManyField("models.Menu", related_name="role_menus")
|
||||
apis = fields.ManyToManyField("models.Api", related_name="role_apis")
|
||||
|
||||
class Meta:
|
||||
table = "role"
|
||||
|
||||
|
||||
class Api(BaseModel, TimestampMixin):
|
||||
path = fields.CharField(max_length=100, description="API路径", index=True)
|
||||
method = fields.CharEnumField(MethodType, description="请求方法", index=True)
|
||||
summary = fields.CharField(max_length=500, description="请求简介", index=True)
|
||||
tags = fields.CharField(max_length=100, description="API标签", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "api"
|
||||
|
||||
|
||||
class Menu(BaseModel, TimestampMixin):
|
||||
name = fields.CharField(max_length=20, description="菜单名称", index=True)
|
||||
remark = fields.JSONField(null=True, description="保留字段")
|
||||
menu_type = fields.CharEnumField(MenuType, null=True, description="菜单类型")
|
||||
icon = fields.CharField(max_length=100, null=True, description="菜单图标")
|
||||
path = fields.CharField(max_length=100, description="菜单路径", index=True)
|
||||
order = fields.IntField(default=0, description="排序", index=True)
|
||||
parent_id = fields.IntField(default=0, description="父菜单ID", index=True)
|
||||
is_hidden = fields.BooleanField(default=False, description="是否隐藏")
|
||||
component = fields.CharField(max_length=100, description="组件")
|
||||
keepalive = fields.BooleanField(default=True, description="存活")
|
||||
redirect = fields.CharField(max_length=100, null=True, description="重定向")
|
||||
|
||||
class Meta:
|
||||
table = "menu"
|
||||
|
||||
|
||||
class Dept(BaseModel, TimestampMixin):
|
||||
name = fields.CharField(max_length=20, unique=True, description="部门名称", index=True)
|
||||
desc = fields.CharField(max_length=500, null=True, description="备注")
|
||||
is_deleted = fields.BooleanField(default=False, description="软删除标记", index=True)
|
||||
order = fields.IntField(default=0, description="排序", index=True)
|
||||
parent_id = fields.IntField(default=0, max_length=10, description="父部门ID", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "dept"
|
||||
|
||||
|
||||
class DeptClosure(BaseModel, TimestampMixin):
|
||||
ancestor = fields.IntField(description="父代", index=True)
|
||||
descendant = fields.IntField(description="子代", index=True)
|
||||
level = fields.IntField(default=0, description="深度", index=True)
|
||||
|
||||
|
||||
class AuditLog(BaseModel, TimestampMixin):
|
||||
user_id = fields.IntField(description="用户ID", index=True)
|
||||
username = fields.CharField(max_length=64, default="", description="用户名称", index=True)
|
||||
module = fields.CharField(max_length=64, default="", description="功能模块", index=True)
|
||||
summary = fields.CharField(max_length=128, default="", description="请求描述", index=True)
|
||||
method = fields.CharField(max_length=10, default="", description="请求方法", index=True)
|
||||
path = fields.CharField(max_length=255, default="", description="请求路径", index=True)
|
||||
status = fields.IntField(default=-1, description="状态码", index=True)
|
||||
response_time = fields.IntField(default=0, description="响应时间(单位ms)", index=True)
|
||||
request_args = fields.JSONField(null=True, description="请求参数")
|
||||
response_body = fields.JSONField(null=True, description="返回数据")
|
||||
|
||||
|
||||
class Codegen(BaseModel, TimestampMixin):
|
||||
"""
|
||||
{
|
||||
"name": "user",
|
||||
"author": "Ly997",
|
||||
"description": "系统用户管理",
|
||||
"version": "1.0.0",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "Long",
|
||||
"primary_key": true,
|
||||
"description": "用户ID",
|
||||
"required": true,
|
||||
"editable": true,
|
||||
"listable": true,
|
||||
"detailable": true,
|
||||
"sortable": true,
|
||||
"filterable": true,
|
||||
"filter_operator": "equal",
|
||||
"display_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "username",
|
||||
"type": "String",
|
||||
"length": 50,
|
||||
"description": "用户名",
|
||||
"required": true,
|
||||
"unique": true,
|
||||
"validations": [
|
||||
{
|
||||
"type": "min_length",
|
||||
"value": 4
|
||||
},
|
||||
{
|
||||
"type": "max_length",
|
||||
"value": 20
|
||||
}
|
||||
],
|
||||
"editable": true,
|
||||
"listable": true,
|
||||
"detailable": true,
|
||||
"sortable": true,
|
||||
"filterable": true,
|
||||
"filter_operator": "equal",
|
||||
"display_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"type": "String",
|
||||
"description": "电子邮箱",
|
||||
"required": true,
|
||||
"validations": [
|
||||
{
|
||||
"type": "email"
|
||||
}
|
||||
],
|
||||
"editable": true,
|
||||
"listable": true,
|
||||
"detailable": true,
|
||||
"sortable": true,
|
||||
"filterable": true,
|
||||
"filter_operator": "equal",
|
||||
"display_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "password_hash",
|
||||
"type": "String",
|
||||
"description": "密码哈希",
|
||||
"required": true,
|
||||
"secret": true,
|
||||
"editable": true,
|
||||
"listable": true,
|
||||
"detailable": true,
|
||||
"sortable": true,
|
||||
"filterable": true,
|
||||
"filter_operator": "equal",
|
||||
"display_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "is_active",
|
||||
"type": "Boolean",
|
||||
"description": "是否激活",
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"listable": true,
|
||||
"detailable": true,
|
||||
"sortable": true,
|
||||
"filterable": true,
|
||||
"filter_operator": "equal",
|
||||
"display_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"type": "DateTime",
|
||||
"description": "创建时间",
|
||||
"auto_now_add": true,
|
||||
"editable": true,
|
||||
"listable": true,
|
||||
"detailable": true,
|
||||
"sortable": true,
|
||||
"filterable": true,
|
||||
"filter_operator": "equal",
|
||||
"display_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"type": "DateTime",
|
||||
"description": "更新时间",
|
||||
"auto_now": true,
|
||||
"editable": true,
|
||||
"listable": true,
|
||||
"detailable": true,
|
||||
"sortable": true,
|
||||
"filterable": true,
|
||||
"filter_operator": "equal",
|
||||
"display_type": "text"
|
||||
}
|
||||
],
|
||||
"relations": [
|
||||
{
|
||||
"name": "roles",
|
||||
"type": "many-to-many",
|
||||
"target": "role",
|
||||
"through": "user_roles",
|
||||
"description": "用户角色关联"
|
||||
},
|
||||
{
|
||||
"name": "posts",
|
||||
"type": "one-to-many",
|
||||
"target": "post",
|
||||
"description": "用户发表的文章"
|
||||
}
|
||||
],
|
||||
"api": {
|
||||
"operations": [
|
||||
"create",
|
||||
"read",
|
||||
"update",
|
||||
"delete",
|
||||
"list"
|
||||
],
|
||||
"base_path": "/users",
|
||||
"auth_required": true,
|
||||
"permissions": {
|
||||
"create": [
|
||||
"admin",
|
||||
"manager"
|
||||
],
|
||||
"delete": [
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"label": "用户管理",
|
||||
"icon": "user",
|
||||
"order": 10,
|
||||
"submenu": [
|
||||
{
|
||||
"label": "用户列表",
|
||||
"path": "/users",
|
||||
"icon": "list"
|
||||
},
|
||||
{
|
||||
"label": "角色管理",
|
||||
"path": "/roles"
|
||||
}
|
||||
]
|
||||
}
|
||||
} """
|
||||
name = Fields.CharField(max_length=64, description="名称", index=True)
|
||||
author = Fields.CharField(max_length=64, description="作者", index=True)
|
||||
description = Fields.CharField(max_length=500, description="描述")
|
||||
version = Fields.CharField(max_length=64, description="版本", index=True)
|
||||
fields = Fields.JSONField(description="字段")
|
||||
relations = Fields.JSONField(description="关系")
|
||||
api = Fields.JSONField(description="API", null=True)
|
||||
menu = Fields.JSONField(description="菜单", null=True)
|
||||
|
||||
class Meta:
|
||||
table = "codegen"
|
||||
|
||||
async def dumps(self, output_type='pydantic'):
|
||||
from app.schemas.codegen import BaseCodegen
|
||||
|
||||
obj_dict = await self.to_dict()
|
||||
# print('type(obj_dict["fields"])', type(obj_dict["fields"]))
|
||||
# print('type(obj_dict["relations"])', type(obj_dict["relations"]))
|
||||
# print('type(obj_dict["api"])', type(obj_dict["api"]))
|
||||
# obj_dict['fields'] = json.loads(obj_dict['fields'])
|
||||
# obj_dict['relations'] = json.loads(obj_dict['relations'])
|
||||
# obj_dict['api'] = json.loads(obj_dict['api'])
|
||||
# obj_dict['menu'] = json.loads(obj_dict['menu'])
|
||||
|
||||
if output_type == 'pydantic': return BaseCodegen(**obj_dict)
|
||||
else : return obj_dict
|
||||
|
||||
|
||||
class Datasource(BaseModel, TimestampMixin):
|
||||
tableName = Fields.CharField(max_length=32, description="名称", index=True)
|
||||
tableComment = Fields.CharField(max_length=64, description="注释", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "datasource"
|
||||
@@ -0,0 +1,119 @@
|
||||
from tortoise import fields
|
||||
|
||||
from .base import BaseModel, TimestampMixin
|
||||
from .enums import TaskStatus, TaskType, ActionType, TaskStatus, ScenarioScope
|
||||
|
||||
|
||||
class Scenario(BaseModel, TimestampMixin):
|
||||
title = fields.CharField(max_length=255, description="场景标题", index=True)
|
||||
trigger = fields.JSONField(description="触发条件(事件/时间)")
|
||||
actions = fields.JSONField(default=list, description="执行动作模板列表")
|
||||
visible = fields.BooleanField(default=True, description="是否可见", index=True)
|
||||
notes = fields.TextField(null=True, description="备注")
|
||||
is_global = fields.BooleanField(default=True, description="是否全局场景", index=True)
|
||||
owner_user_id = fields.CharField(max_length=64, null=True, description="归属用户ID(非全局时有效)", index=True)
|
||||
enabled = fields.BooleanField(default=True, description="是否启用", index=True)
|
||||
scope = fields.CharEnumField(ScenarioScope, default=ScenarioScope.ALL, description="作用角色范围", index=True)
|
||||
due_days = fields.IntField(null=True, description="截止时间(天)")
|
||||
|
||||
class Meta:
|
||||
table = "automation_scenario"
|
||||
indexes = [
|
||||
["is_global", "enabled"],
|
||||
["owner_user_id", "enabled"],
|
||||
]
|
||||
|
||||
"""
|
||||
CREATE TABLE automation_scenario_trigger_index (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
scenario_id BIGINT NOT NULL,
|
||||
event_name VARCHAR(64) NOT NULL COMMENT '监听的事件名,如 group_chat_updated',
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||
is_global TINYINT(1) NOT NULL DEFAULT 1,
|
||||
owner_user_id VARCHAR(64) DEFAULT NULL,
|
||||
scope VARCHAR(8) NOT NULL DEFAULT 'all',
|
||||
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_event_enabled (event_name, enabled),
|
||||
KEY idx_scenario_id (scenario_id),
|
||||
CONSTRAINT fk_scenario_def FOREIGN KEY (scenario_id) REFERENCES automation_scenario(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
"""
|
||||
class ScenarioTriggerIndex(BaseModel, TimestampMixin):
|
||||
scenario_id = fields.BigIntField(description="场景ID")
|
||||
type = fields.CharEnumField(TaskType, default=TaskType.EVENT, description="事件类型", index=True)
|
||||
event_name = fields.CharField(max_length=64, description="监听的事件名,如 group_chat_updated", index=True)
|
||||
enabled = fields.BooleanField(default=True, description="是否启用", index=True)
|
||||
is_global = fields.BooleanField(default=True, description="是否全局场景", index=True)
|
||||
owner_user_id = fields.CharField(max_length=64, null=True, description="归属用户ID(非全局时有效)", index=True)
|
||||
scope = fields.CharEnumField(ScenarioScope, default=ScenarioScope.ALL, description="作用角色范围")
|
||||
|
||||
class Meta:
|
||||
table = "automation_scenario_trigger_index"
|
||||
|
||||
|
||||
class Task(BaseModel, TimestampMixin):
|
||||
title = fields.CharField(max_length=255, description="待办标题", index=True)
|
||||
event_data = fields.JSONField(default=dict, description="事件数据")
|
||||
notes = fields.TextField(null=True, description="备注")
|
||||
reason = fields.TextField(null=True, description="触发原因")
|
||||
|
||||
completed_at = fields.DatetimeField(null=True, description="完成时间")
|
||||
|
||||
# 关联字段
|
||||
source_scenario = fields.ForeignKeyField(
|
||||
"models.Scenario",
|
||||
related_name="generated_tasks",
|
||||
null=True,
|
||||
on_delete=fields.SET_NULL,
|
||||
description="来源场景"
|
||||
)
|
||||
|
||||
related_order_id = fields.CharField(max_length=64, null=True, description="关联订单ID", index=True)
|
||||
owner_user_id = fields.CharField(max_length=64, null=True, description="归属用户ID", index=True)
|
||||
|
||||
# 分配与状态
|
||||
assignee_user_id = fields.CharField(max_length=64, null=True, description="指派人用户ID", index=True)
|
||||
assignee_username = fields.CharField(max_length=64, null=True, description="指派人用户名", index=True)
|
||||
status = fields.CharEnumField(TaskStatus, default=TaskStatus.PENDING, description="任务状态", index=True)
|
||||
|
||||
# 自动化控制
|
||||
due_at = fields.DatetimeField(null=True, description="截止时间")
|
||||
auto_closeable = fields.BooleanField(default=True, description="是否允许自动消除")
|
||||
closed_by = fields.CharField(max_length=32, null=True, description="关闭方式: manual/auto/expired", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "automation_task"
|
||||
indexes = [
|
||||
["assignee_user_id", "status"],
|
||||
]
|
||||
|
||||
|
||||
class Action(BaseModel, TimestampMixin):
|
||||
type = fields.CharEnumField(ActionType, null=True, description="操作类型", index=True)
|
||||
detail = fields.JSONField(default={}, description="操作详情")
|
||||
done = fields.BooleanField(default=False, description="是否已完成", index=True)
|
||||
done_at = fields.DatetimeField(null=True, description="完成时间")
|
||||
result = fields.JSONField(default={}, description="操作结果")
|
||||
userid = fields.CharField(max_length=64, null=True, description="操作用户ID", index=True)
|
||||
username = fields.CharField(max_length=64, null=True, description="操作用户名称", index=True)
|
||||
notes = fields.TextField(null=True, description="操作备注")
|
||||
|
||||
# 硬外键:必须属于一个 Task
|
||||
task = fields.ForeignKeyField(
|
||||
"models.Task",
|
||||
related_name="actions",
|
||||
null=True, # ← 允许为空
|
||||
on_delete=fields.SET_NULL, # ← 注意:CASCADE 不能和 null=True 同时用于 SET_NULL
|
||||
description="所属任务"
|
||||
)
|
||||
|
||||
# 可选:记录来自哪个动作模板(场景中的定义)
|
||||
action_template_id = fields.CharField(max_length=64, null=True, description="操作模板ID", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "automation_action"
|
||||
indexes = [
|
||||
["task_id", "done"], # 优化查询未完成动作
|
||||
["type", "done"],
|
||||
]
|
||||
@@ -0,0 +1,68 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
from tortoise import fields, models
|
||||
|
||||
from app.settings import settings
|
||||
|
||||
|
||||
class BaseModel(models.Model):
|
||||
id = fields.BigIntField(pk=True, index=True)
|
||||
|
||||
async def to_dict(self, m2m: bool = False, exclude_fields: list[str] | None = None):
|
||||
if exclude_fields is None:
|
||||
exclude_fields = []
|
||||
|
||||
d = {}
|
||||
for field in self._meta.db_fields:
|
||||
if field not in exclude_fields:
|
||||
value = getattr(self, field)
|
||||
if isinstance(value, datetime):
|
||||
value = value.strftime(settings.DATETIME_FORMAT)
|
||||
d[field] = value
|
||||
|
||||
if m2m:
|
||||
tasks = [
|
||||
self.__fetch_m2m_field(field, exclude_fields)
|
||||
for field in self._meta.m2m_fields
|
||||
if field not in exclude_fields
|
||||
]
|
||||
results = await asyncio.gather(*tasks)
|
||||
for field, values in results:
|
||||
d[field] = values
|
||||
|
||||
return d
|
||||
|
||||
async def __fetch_m2m_field(self, field, exclude_fields):
|
||||
values = await getattr(self, field).all().values()
|
||||
formatted_values = []
|
||||
|
||||
for value in values:
|
||||
formatted_value = {}
|
||||
for k, v in value.items():
|
||||
if k not in exclude_fields:
|
||||
if isinstance(v, datetime):
|
||||
formatted_value[k] = v.strftime(settings.DATETIME_FORMAT)
|
||||
else:
|
||||
formatted_value[k] = v
|
||||
formatted_values.append(formatted_value)
|
||||
|
||||
return field, formatted_values
|
||||
|
||||
@classmethod
|
||||
def get_all_keys(cls, exclude_fields: list[str] | None = None):
|
||||
if exclude_fields is None:
|
||||
exclude_fields = []
|
||||
return [field for field in cls._meta.fields_map.keys() if field not in exclude_fields]
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class UUIDModel:
|
||||
uuid = fields.UUIDField(unique=True, pk=False, index=True)
|
||||
|
||||
|
||||
class TimestampMixin:
|
||||
created_at = fields.DatetimeField(auto_now_add=True, index=True)
|
||||
updated_at = fields.DatetimeField(auto_now=True, index=True)
|
||||
@@ -0,0 +1,64 @@
|
||||
from enum import Enum, StrEnum
|
||||
|
||||
|
||||
class EnumBase(Enum):
|
||||
@classmethod
|
||||
def get_member_values(cls):
|
||||
return [item.value for item in cls._member_map_.values()]
|
||||
|
||||
@classmethod
|
||||
def get_member_names(cls):
|
||||
return [name for name in cls._member_names_]
|
||||
|
||||
|
||||
class MethodType(StrEnum):
|
||||
GET = "GET"
|
||||
POST = "POST"
|
||||
PUT = "PUT"
|
||||
DELETE = "DELETE"
|
||||
PATCH = "PATCH"
|
||||
|
||||
class TaskStatus(Enum):
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
SUCCESS = "success"
|
||||
FAILED = "failed"
|
||||
CANCELLED = "cancelled"
|
||||
EXPIRED = "expired"
|
||||
|
||||
class TaskType(Enum):
|
||||
DECODE_ORDER = "decode_order"
|
||||
EVENT = "event"
|
||||
TIMER = "timer"
|
||||
|
||||
class ScenarioScope(str, Enum):
|
||||
ALL = "all"
|
||||
PERSONAL = "personal"
|
||||
KEFU = "kefu" # 接待员
|
||||
FOLLOW = "follow" # 跟单员
|
||||
DESIGNER = "designer" # 设计师
|
||||
|
||||
class RoleType(StrEnum):
|
||||
BUYER = "buyer"
|
||||
DESIGER = "desiger"
|
||||
FOLLOWER = "follower"
|
||||
ADMIN = "admin"
|
||||
KEFU = "taobao_kefu"
|
||||
|
||||
class MsgType(StrEnum):
|
||||
NEW_ORDER = "new_order"
|
||||
COMMENT = "comment"
|
||||
SYSTEM = "system"
|
||||
NOTIFY = "notify"
|
||||
|
||||
class ActionType(StrEnum):
|
||||
CREATE_GROUP = "create_group"
|
||||
VIEW_ERP_LOG = "view_erp_log"
|
||||
BIND_USER = "bind_user"
|
||||
BIND_ORDER = "bind_order"
|
||||
WRITE_REMARK = "write_remark"
|
||||
SEND_WECHAT_NOTIFY = "send_notify"
|
||||
# 清理群聊
|
||||
CLEAN_GROUP = "clean_group"
|
||||
# 设置群管理员
|
||||
SET_GROUP_ADMIN = "set_group_admin"
|
||||
@@ -0,0 +1,72 @@
|
||||
from tortoise import fields
|
||||
|
||||
from .base import BaseModel, TimestampMixin
|
||||
from .enums import MsgType, ActionType
|
||||
|
||||
class Msg(BaseModel, TimestampMixin):
|
||||
hash_id = fields.CharField(max_length=64, null=True, description="消息哈希ID", index=True, unique=True)
|
||||
title = fields.CharField(max_length=64, null=True, description="消息标题", index=True)
|
||||
content = fields.TextField(null=True, description="消息内容")
|
||||
detail = fields.JSONField(default={}, description="消息详情")
|
||||
is_send = fields.BooleanField(default=False, description="是否已发送", index=True)
|
||||
send_at = fields.DatetimeField(null=True, description="发送时间")
|
||||
is_read = fields.BooleanField(default=False, description="是否已读", index=True)
|
||||
read_at = fields.DatetimeField(null=True, description="已读时间")
|
||||
is_delete = fields.BooleanField(default=False, description="是否已删除", index=True)
|
||||
delete_at = fields.DatetimeField(null=True, description="删除时间")
|
||||
type = fields.CharEnumField(MsgType, null=True, description="消息类型", index=True)
|
||||
owner_id = fields.CharField(max_length=64, null=True, description="消息所有者ID", index=True)
|
||||
owner_name = fields.CharField(max_length=64, null=True, description="消息所有者名称", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "system_msg"
|
||||
|
||||
|
||||
class Follow(BaseModel, TimestampMixin):
|
||||
order_id = fields.CharField(max_length=64, null=True, description="订单ID", index=True)
|
||||
order_title = fields.TextField(null=True, description="订单标题")
|
||||
order_pic = fields.CharField(max_length=256, null=True, description="订单图片")
|
||||
pay_time = fields.DatetimeField(null=True, description="支付时间", index=True)
|
||||
pay_price = fields.FloatField(null=True, description="支付金额", index=True)
|
||||
remark = fields.TextField(null=True, description="erp额外备注")
|
||||
|
||||
customer_id = fields.CharField(max_length=64, null=True, description="客户ID", index=True)
|
||||
customer_name = fields.CharField(max_length=64, null=True, description="客户名称", index=True)
|
||||
customer_taobao_id = fields.CharField(max_length=64, null=True, description="客户淘宝ID", index=True)
|
||||
|
||||
staff_id = fields.CharField(max_length=64, null=True, description="企微员工ID", index=True)
|
||||
staff_name = fields.CharField(max_length=64, null=True, description="企微员工名称", index=True)
|
||||
staff_time = fields.DatetimeField(null=True, description="添加时间", index=True)
|
||||
|
||||
designer_id = fields.CharField(max_length=64, null=True, description="设计师ID", index=True)
|
||||
designer_name = fields.CharField(max_length=64, null=True, description="设计师名称", index=True)
|
||||
|
||||
shop_name = fields.CharField(max_length=64, null=True, description="店铺名称", index=True)
|
||||
kefu_id = fields.CharField(max_length=64, null=True, description="淘宝客服ID", index=True)
|
||||
kefu_name = fields.CharField(max_length=64, null=True, description="淘宝客服名称", index=True)
|
||||
|
||||
is_delete = fields.BooleanField(default=False, description="是否已删除", index=True)
|
||||
|
||||
feishu_record_id = fields.CharField(max_length=64, null=True, description="飞书记录ID", index=True)
|
||||
is_add_to_feishu = fields.BooleanField(default=False, description="是否已添加到飞书", index=True)
|
||||
is_update_to_feishu = fields.BooleanField(default=False, description="是否已更新到飞书", index=True)
|
||||
is_delete_from_feishu = fields.BooleanField(default=False, description="是否已从飞书删除", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "system_follow"
|
||||
|
||||
def to_feishu_record(self, fields: dict=None):
|
||||
fields = fields or {}
|
||||
return {
|
||||
"record_id": self.feishu_record_id,
|
||||
"fields": {
|
||||
"付款时间": self.pay_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"店铺名": self.shop_name,
|
||||
"成交客服": self.kefu_name,
|
||||
"旺旺id": self.customer_name,
|
||||
"添加企微客服": self.staff_name,
|
||||
"订单编号": self.order_id,
|
||||
"设计师": self.designer_name,
|
||||
**fields
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
from tortoise import fields
|
||||
|
||||
from .base import BaseModel, TimestampMixin
|
||||
from .enums import RoleType
|
||||
|
||||
class WeixinUser(BaseModel, TimestampMixin):
|
||||
# 自动成为订单关联方
|
||||
auto_relate = fields.BooleanField(default=False, description="是否自动成为订单关联方")
|
||||
userid = fields.CharField(max_length=64, description="用户微信id", index=True) # acctid
|
||||
corp_id = fields.CharField(max_length=64, description="企业微信id", default="1970325009092879", index=True)
|
||||
vid = fields.CharField(max_length=64, description="用户微信id", default='', index=True)
|
||||
wx_id_hash = fields.CharField(max_length=64, null=True, description="微信id hash")
|
||||
is_quit = fields.BooleanField(default=False, description="是否离职")
|
||||
has_external_user_permit = fields.BooleanField(default=False, description="是否具有对外联系权限")
|
||||
wx_nick_name = fields.CharField(max_length=64, null=True, description="微信昵称")
|
||||
account = fields.CharField(max_length=64, null=True, description="账号")
|
||||
|
||||
position = fields.CharField(max_length=64, null=True, description="职位")
|
||||
role = fields.CharEnumField(RoleType, null=True, description="用户角色", index=True)
|
||||
depart_ids = fields.JSONField(default=[], description="部门ID列表")
|
||||
|
||||
username = fields.CharField(max_length=64, null=True, description="用户微信名称", index=True)
|
||||
english_name = fields.CharField(max_length=64, null=True, description="英文名")
|
||||
name = fields.CharField(max_length=64, null=True, description="用户名称", index=True)
|
||||
realname = fields.CharField(max_length=64, null=True, description="用户真实名字", index=True)
|
||||
alias = fields.CharField(max_length=30, null=True, description="别名")
|
||||
avatar = fields.CharField(max_length=512, null=True, description="头像")
|
||||
mobile = fields.CharField(max_length=11, null=True, description="手机号")
|
||||
email = fields.CharField(max_length=64, null=True, description="邮箱")
|
||||
gender = fields.IntField(null=True, description="性别")
|
||||
|
||||
erp_id = fields.IntField(null=True, description="ERP中的用户ID", index=True)
|
||||
erp_name = fields.CharField(max_length=64, null=True, description="ERP中的用户名称")
|
||||
crm_id = fields.IntField(null=True, description="星云有客中的用户ID", index=True)
|
||||
crm_name = fields.CharField(max_length=64, null=True, description="星云有客中的用户名称")
|
||||
|
||||
def to_dict(self, exclude_fields=None, include_sensitive=False):
|
||||
"""
|
||||
自定义字典转换行为
|
||||
|
||||
Args:
|
||||
exclude_fields: 要排除的字段列表
|
||||
include_sensitive: 是否包含敏感信息(如手机号、邮箱等)
|
||||
|
||||
Returns:
|
||||
dict: 转换后的字典
|
||||
"""
|
||||
exclude_fields = exclude_fields or ['staff_memberships']
|
||||
data = {}
|
||||
|
||||
# 获取所有字段名
|
||||
model_fields = self._meta.fields_map.keys()
|
||||
|
||||
for field_name in model_fields:
|
||||
if field_name in exclude_fields:
|
||||
continue
|
||||
|
||||
# 如果不包含敏感信息,则跳过敏感字段
|
||||
if not include_sensitive and field_name in ['mobile', 'email']:
|
||||
continue
|
||||
|
||||
value = getattr(self, field_name)
|
||||
|
||||
# 处理枚举字段
|
||||
if field_name == 'role' and value is not None:
|
||||
data[field_name] = value.value if hasattr(value, 'value') else value
|
||||
# 处理布尔字段的默认值显示
|
||||
elif field_name == 'is_quit':
|
||||
data[field_name] = bool(value) if value is not None else False
|
||||
elif field_name == 'has_external_user_permit':
|
||||
data[field_name] = bool(value) if value is not None else False
|
||||
else:
|
||||
data[field_name] = value
|
||||
|
||||
return data
|
||||
|
||||
def to_public_dict(self):
|
||||
"""
|
||||
返回公开信息的字典(不包含敏感信息)
|
||||
"""
|
||||
sensitive_fields = ['mobile', 'email']
|
||||
exclude_fields = ['wx_id_hash'] # 可能还有其他不想暴露的字段
|
||||
all_exclude = sensitive_fields + exclude_fields
|
||||
|
||||
return self.to_dict(exclude_fields=all_exclude, include_sensitive=False)
|
||||
|
||||
def to_detail_dict(self):
|
||||
"""
|
||||
返回详细信息的字典(包含所有信息)
|
||||
"""
|
||||
return self.to_dict(include_sensitive=True)
|
||||
|
||||
def to_safe_dict(self, visible_fields=None):
|
||||
"""
|
||||
返回指定字段的安全字典
|
||||
|
||||
Args:
|
||||
visible_fields: 指定要包含的字段列表,如果为None则使用默认安全字段
|
||||
"""
|
||||
if visible_fields is None:
|
||||
# 默认的安全字段(不包含敏感信息)
|
||||
visible_fields = [
|
||||
'userid', 'vid', 'wx_nick_name', 'username', 'name',
|
||||
'realname', 'alias', 'avatar', 'position', 'role',
|
||||
'depart_ids', 'erpid', 'crmid', 'created_at', 'updated_at'
|
||||
]
|
||||
|
||||
data = {}
|
||||
for field_name in visible_fields:
|
||||
if hasattr(self, field_name):
|
||||
value = getattr(self, field_name)
|
||||
if field_name == 'role' and value is not None:
|
||||
data[field_name] = value.value if hasattr(value, 'value') else value
|
||||
else:
|
||||
data[field_name] = value
|
||||
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
table = "weixin_user"
|
||||
|
||||
class WeixinGroupChat(BaseModel, TimestampMixin):
|
||||
chat_id = fields.CharField(max_length=64, description="群聊ID", index=True)
|
||||
xingyun_chat_id = fields.IntField(null=True, description="星云中的群聊ID", index=True)
|
||||
name = fields.CharField(max_length=64, null=True, description="群聊名称")
|
||||
create_time = fields.IntField(description="创建时间")
|
||||
admin_list = fields.JSONField(default=[], description="所有管理员")
|
||||
owner = fields.CharField(max_length=64, null=True, description="群主")
|
||||
member_version = fields.CharField(max_length=64, null=True, description="群成员版本")
|
||||
external_user_count = fields.IntField(null=True, description="外部群成员数量")
|
||||
external_member_list = fields.JSONField(default=[], description="外部群成员列表")
|
||||
internal_member_count = fields.IntField(null=True, description="内部群成员数量")
|
||||
internal_member_list = fields.JSONField(default=[], description="内部群成员列表")
|
||||
avatars = fields.JSONField(default=[], description="群成员头像")
|
||||
|
||||
class Meta:
|
||||
table = "weixin_group_chat"
|
||||
|
||||
class WeixinCustomer(BaseModel, TimestampMixin):
|
||||
order_id = fields.CharField(max_length=64, null=True, description="订单ID", index=True)
|
||||
shop_name = fields.CharField(max_length=64, null=True, description="店铺名称")
|
||||
weixin_id = fields.CharField(max_length=64, null=True, description="微信体系中的id", index=True)
|
||||
weixin_name = fields.CharField(max_length=64, null=True, description="微信体系中的用户名")
|
||||
weixin_unionid = fields.CharField(max_length=64, null=True, description="微信体系中的unionid", index=True)
|
||||
weixin_avatar = fields.CharField(max_length=512, null=True, description="微信头像")
|
||||
xingyun_id = fields.IntField(null=True, description="星云有客中的用户ID", index=True)
|
||||
xingyun_sex = fields.IntField(null=True, description="星云有客中的用户性别", index=True)
|
||||
xingyun_name = fields.CharField(max_length=64, null=True, description="星云有客中的用户名")
|
||||
xingyun_avatar = fields.CharField(max_length=512, null=True, description="星云有客头像")
|
||||
xingyun_tags = fields.JSONField(default=[], null=True, description="星云用户标签")
|
||||
xingyun_external_userid = fields.CharField(max_length=64, null=True, description="星云有客中的外部用户ID")
|
||||
xingyun_sync = fields.BooleanField(default=False, description="是否已同步到星云", index=True)
|
||||
# erp_id = fields.IntField(null=True, description="ERP中的客户ID", index=True)
|
||||
# erp_name = fields.CharField(max_length=64, null=True, description="ERP中的客户名称")
|
||||
# erp_avatar = fields.CharField(max_length=512, null=True, description="ERP头像")
|
||||
taobao_id = fields.CharField(max_length=64, null=True, description="淘宝中的用户ID", index=True)
|
||||
taobao_name = fields.CharField(max_length=64, null=True, description="淘宝中的用户名")
|
||||
taobao_avatar = fields.CharField(max_length=512, null=True, description="淘宝头像")
|
||||
need_confirm = fields.BooleanField(null=True, description="是否需要确认")
|
||||
extra = fields.JSONField(default={}, description="额外信息")
|
||||
|
||||
# 使用 through 指向自定义中间模型
|
||||
groups = fields.ManyToManyField(
|
||||
"models.WeixinGroupChat",
|
||||
through="customer_group", # 必须是中间模型的 table 名(或模型名)
|
||||
related_name="customers"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def orm_format(cls, data: dict):
|
||||
# 将不在字段中的字段保存到extra字段中
|
||||
all_fields = set(cls._meta.fields_map.keys())
|
||||
|
||||
extra = {}
|
||||
data_in = {"extra": extra}
|
||||
for k, v in data.copy().items():
|
||||
if k not in all_fields:
|
||||
extra[k] = v
|
||||
else:
|
||||
data_in[k] = v
|
||||
|
||||
id = data_in.pop('id', None)
|
||||
if id: data_in['platform_id'] = id
|
||||
return data_in
|
||||
|
||||
def to_dict(self, *args, **kwargs):
|
||||
# 类似于WeixinUser参数
|
||||
return {
|
||||
"id": self.id,
|
||||
"userid": self.weixin_id,
|
||||
"name": self.taobao_name,
|
||||
"avatar": self.xingyun_avatar,
|
||||
"weixin_name": self.weixin_name,
|
||||
"role": 'buyer',
|
||||
"is_customer": True,
|
||||
}
|
||||
# data = await super().to_dict(*args, **kwargs)
|
||||
# data['id'] = data.pop('platform_id', None)
|
||||
# data.update(data.pop('extra', {}))
|
||||
# return data
|
||||
|
||||
def dump_dict(self):
|
||||
all_fields = set(self._meta.fields_map.keys())
|
||||
data = {}
|
||||
for k, v in self.__dict__.items():
|
||||
if k in all_fields:
|
||||
data[k] = v
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def create_bind(cls, data: dict):
|
||||
# 将不在字段中的字段保存到extra字段中
|
||||
data_in = cls.orm_format(data)
|
||||
return cls(**data_in)
|
||||
|
||||
class Meta:
|
||||
table = "weixin_customer"
|
||||
|
||||
# === 中间模型:CustomerGroup ===
|
||||
class CustomerGroup(BaseModel, TimestampMixin):
|
||||
"""顾客与群聊的关联关系(带额外信息)"""
|
||||
|
||||
# 外键指向顾客
|
||||
customer = fields.ForeignKeyField(
|
||||
"models.WeixinCustomer",
|
||||
related_name="group_memberships" # 从 Customer 反向查关系
|
||||
)
|
||||
|
||||
# 外键指向群聊
|
||||
group = fields.ForeignKeyField(
|
||||
"models.WeixinGroupChat",
|
||||
related_name="customer_memberships" # 从 Group 反向查关系
|
||||
)
|
||||
|
||||
# 外键指向群聊
|
||||
staff = fields.ForeignKeyField(
|
||||
"models.WeixinUser",
|
||||
related_name="staff_memberships" # 从 User 反向查关系
|
||||
)
|
||||
|
||||
# 额外字段
|
||||
join_time = fields.DatetimeField(auto_now_add=True, description="入群时间", index=True)
|
||||
role = fields.CharField(
|
||||
max_length=32,
|
||||
default="member",
|
||||
description="群内角色:member / admin / owner",
|
||||
index=True
|
||||
)
|
||||
|
||||
staff_userid = fields.CharField(max_length=64, null=True, description="客服ID", index=True)
|
||||
customer_userid = fields.CharField(max_length=64, null=True, description="顾客的ID", index=True)
|
||||
group_chatid = fields.CharField(max_length=64, null=True, description="群聊ID", index=True)
|
||||
order_id = fields.CharField(max_length=64, null=True, description="订单ID", index=True)
|
||||
shop_name = fields.CharField(max_length=64, null=True, description="店铺名称", index=True)
|
||||
remark = fields.CharField(max_length=255, null=True, description="备注")
|
||||
|
||||
class Meta:
|
||||
table = "customer_group"
|
||||
# 确保同一个顾客不能重复加入同一个群
|
||||
unique_together = ("customer", "group")
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
train/
|
||||
# 忽略 models 目录下的所有文件和子目录
|
||||
models/*
|
||||
|
||||
# 递归保留所有子目录中的 best_model 目录
|
||||
models/订单尺寸识别/*
|
||||
!models/订单尺寸识别/
|
||||
!models/订单尺寸识别/best_model/
|
||||
|
||||
models/定制印刷品-订单描述/*
|
||||
!models/定制印刷品-订单描述/
|
||||
!models/定制印刷品-订单描述/best_model/
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,135 @@
|
||||
[paths]
|
||||
train = null
|
||||
dev = null
|
||||
vectors = null
|
||||
init_tok2vec = null
|
||||
|
||||
[system]
|
||||
seed = 0
|
||||
gpu_allocator = null
|
||||
|
||||
[nlp]
|
||||
lang = "zh"
|
||||
pipeline = ["ner"]
|
||||
disabled = []
|
||||
before_creation = null
|
||||
after_creation = null
|
||||
after_pipeline_creation = null
|
||||
batch_size = 1000
|
||||
vectors = {"@vectors":"spacy.Vectors.v1"}
|
||||
|
||||
[nlp.tokenizer]
|
||||
@tokenizers = "spacy.zh.ChineseTokenizer"
|
||||
segmenter = "char"
|
||||
|
||||
[components]
|
||||
|
||||
[components.ner]
|
||||
factory = "ner"
|
||||
incorrect_spans_key = null
|
||||
moves = null
|
||||
scorer = {"@scorers":"spacy.ner_scorer.v1"}
|
||||
update_with_oracle_cut_size = 100
|
||||
|
||||
[components.ner.model]
|
||||
@architectures = "spacy.TransitionBasedParser.v2"
|
||||
state_type = "ner"
|
||||
extra_state_tokens = false
|
||||
hidden_width = 64
|
||||
maxout_pieces = 2
|
||||
use_upper = true
|
||||
nO = null
|
||||
|
||||
[components.ner.model.tok2vec]
|
||||
@architectures = "spacy.HashEmbedCNN.v2"
|
||||
pretrained_vectors = null
|
||||
width = 96
|
||||
depth = 4
|
||||
embed_size = 2000
|
||||
window_size = 1
|
||||
maxout_pieces = 3
|
||||
subword_features = true
|
||||
|
||||
[corpora]
|
||||
|
||||
[corpora.dev]
|
||||
@readers = "spacy.Corpus.v1"
|
||||
path = ${paths.dev}
|
||||
gold_preproc = false
|
||||
max_length = 0
|
||||
limit = 0
|
||||
augmenter = null
|
||||
|
||||
[corpora.train]
|
||||
@readers = "spacy.Corpus.v1"
|
||||
path = ${paths.train}
|
||||
gold_preproc = false
|
||||
max_length = 0
|
||||
limit = 0
|
||||
augmenter = null
|
||||
|
||||
[training]
|
||||
seed = ${system.seed}
|
||||
gpu_allocator = ${system.gpu_allocator}
|
||||
dropout = 0.1
|
||||
accumulate_gradient = 1
|
||||
patience = 1600
|
||||
max_epochs = 0
|
||||
max_steps = 20000
|
||||
eval_frequency = 200
|
||||
frozen_components = []
|
||||
annotating_components = []
|
||||
dev_corpus = "corpora.dev"
|
||||
train_corpus = "corpora.train"
|
||||
before_to_disk = null
|
||||
before_update = null
|
||||
|
||||
[training.batcher]
|
||||
@batchers = "spacy.batch_by_words.v1"
|
||||
discard_oversize = false
|
||||
tolerance = 0.2
|
||||
get_length = null
|
||||
|
||||
[training.batcher.size]
|
||||
@schedules = "compounding.v1"
|
||||
start = 100
|
||||
stop = 1000
|
||||
compound = 1.001
|
||||
t = 0.0
|
||||
|
||||
[training.logger]
|
||||
@loggers = "spacy.ConsoleLogger.v1"
|
||||
progress_bar = false
|
||||
|
||||
[training.optimizer]
|
||||
@optimizers = "Adam.v1"
|
||||
beta1 = 0.9
|
||||
beta2 = 0.999
|
||||
L2_is_weight_decay = true
|
||||
L2 = 0.01
|
||||
grad_clip = 1.0
|
||||
use_averages = false
|
||||
eps = 0.00000001
|
||||
learn_rate = 0.001
|
||||
|
||||
[training.score_weights]
|
||||
ents_f = 1.0
|
||||
ents_p = 0.0
|
||||
ents_r = 0.0
|
||||
ents_per_type = null
|
||||
|
||||
[pretraining]
|
||||
|
||||
[initialize]
|
||||
vectors = ${paths.vectors}
|
||||
init_tok2vec = ${paths.init_tok2vec}
|
||||
vocab_data = null
|
||||
lookups = null
|
||||
before_init = null
|
||||
after_init = null
|
||||
|
||||
[initialize.components]
|
||||
|
||||
[initialize.tokenizer]
|
||||
pkuseg_model = null
|
||||
pkuseg_user_dict = "default"
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"lang":"zh",
|
||||
"name":"pipeline",
|
||||
"version":"0.0.0",
|
||||
"spacy_version":">=3.8.7,<3.9.0",
|
||||
"description":"",
|
||||
"author":"",
|
||||
"email":"",
|
||||
"url":"",
|
||||
"license":"",
|
||||
"spacy_git_version":"4b65aa7",
|
||||
"vectors":{
|
||||
"width":0,
|
||||
"vectors":0,
|
||||
"keys":0,
|
||||
"name":null,
|
||||
"mode":"default"
|
||||
},
|
||||
"labels":{
|
||||
"ner":[
|
||||
"\u4ea7\u54c1",
|
||||
"\u522e\u522e\u819c\u5c3a\u5bf8",
|
||||
"\u52a0\u6025",
|
||||
"\u5355\u53f7",
|
||||
"\u5c3a\u5bf8",
|
||||
"\u5de5\u827a",
|
||||
"\u6570\u91cf",
|
||||
"\u6750\u8d28",
|
||||
"\u7528\u6237\u4fe1\u606f"
|
||||
]
|
||||
},
|
||||
"pipeline":[
|
||||
"ner"
|
||||
],
|
||||
"components":[
|
||||
"ner"
|
||||
],
|
||||
"disabled":[
|
||||
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"moves":null,
|
||||
"update_with_oracle_cut_size":100,
|
||||
"multitasks":[
|
||||
|
||||
],
|
||||
"min_action_freq":1,
|
||||
"learn_tokens":false,
|
||||
"beam_width":1,
|
||||
"beam_density":0.0,
|
||||
"beam_update_prob":0.0,
|
||||
"incorrect_spans_key":null
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
‚¥movesÚ0{"0":{},"1":{"\u52a0\u6025":-1,"\u7528\u6237\u4fe1\u606f":-2,"\u5355\u53f7":-3,"\u6750\u8d28":-4,"\u522e\u522e\u819c\u5c3a\u5bf8":-5,"\u5de5\u827a":-6,"\u6570\u91cf":-7,"\u5c3a\u5bf8":-8,"\u4ea7\u54c1":-9},"2":{"\u52a0\u6025":-1,"\u7528\u6237\u4fe1\u606f":-2,"\u5355\u53f7":-3,"\u6750\u8d28":-4,"\u522e\u522e\u819c\u5c3a\u5bf8":-5,"\u5de5\u827a":-6,"\u6570\u91cf":-7,"\u5c3a\u5bf8":-8,"\u4ea7\u54c1":-9},"3":{"\u52a0\u6025":-1,"\u7528\u6237\u4fe1\u606f":-2,"\u5355\u53f7":-3,"\u6750\u8d28":-4,"\u522e\u522e\u819c\u5c3a\u5bf8":-5,"\u5de5\u827a":-6,"\u6570\u91cf":-7,"\u5c3a\u5bf8":-8,"\u4ea7\u54c1":-9},"4":{"":1,"\u52a0\u6025":-1,"\u7528\u6237\u4fe1\u606f":-2,"\u5355\u53f7":-3,"\u6750\u8d28":-4,"\u522e\u522e\u819c\u5c3a\u5bf8":-5,"\u5de5\u827a":-6,"\u6570\u91cf":-7,"\u5c3a\u5bf8":-8,"\u4ea7\u54c1":-9},"5":{"":1}}£cfg�§neg_keyÀ
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"segmenter":"char"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
�
|
||||
@@ -0,0 +1 @@
|
||||
�
|
||||
@@ -0,0 +1,675 @@
|
||||
[
|
||||
" ",
|
||||
"(",
|
||||
")",
|
||||
"+",
|
||||
",",
|
||||
"-",
|
||||
".",
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
":",
|
||||
";",
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G",
|
||||
"H",
|
||||
"J",
|
||||
"K",
|
||||
"L",
|
||||
"M",
|
||||
"N",
|
||||
"P",
|
||||
"Q",
|
||||
"R",
|
||||
"ROOT",
|
||||
"S",
|
||||
"T",
|
||||
"U",
|
||||
"V",
|
||||
"Y",
|
||||
"Z",
|
||||
"[",
|
||||
"]",
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"g",
|
||||
"h",
|
||||
"i",
|
||||
"j",
|
||||
"k",
|
||||
"l",
|
||||
"m",
|
||||
"n",
|
||||
"o",
|
||||
"p",
|
||||
"q",
|
||||
"r",
|
||||
"s",
|
||||
"t",
|
||||
"u",
|
||||
"v",
|
||||
"w",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"zh",
|
||||
"\u00d7",
|
||||
"\u2606",
|
||||
"\u3010",
|
||||
"\u3011",
|
||||
"\u4e00",
|
||||
"\u4e03",
|
||||
"\u4e07",
|
||||
"\u4e09",
|
||||
"\u4e0a",
|
||||
"\u4e0b",
|
||||
"\u4e0d",
|
||||
"\u4e0e",
|
||||
"\u4e13",
|
||||
"\u4e16",
|
||||
"\u4e1c",
|
||||
"\u4e1d",
|
||||
"\u4e24",
|
||||
"\u4e2a",
|
||||
"\u4e2b",
|
||||
"\u4e2d",
|
||||
"\u4e30",
|
||||
"\u4e3a",
|
||||
"\u4e3b",
|
||||
"\u4e3d",
|
||||
"\u4e3e",
|
||||
"\u4e43",
|
||||
"\u4e48",
|
||||
"\u4e50",
|
||||
"\u4e86",
|
||||
"\u4e8c",
|
||||
"\u4e91",
|
||||
"\u4e92",
|
||||
"\u4e94",
|
||||
"\u4ea7\u54c1",
|
||||
"\u4eac",
|
||||
"\u4eae",
|
||||
"\u4eba",
|
||||
"\u4ec0",
|
||||
"\u4ed8",
|
||||
"\u4ef6",
|
||||
"\u4f0d",
|
||||
"\u4f1f",
|
||||
"\u4f38",
|
||||
"\u4f3c",
|
||||
"\u4f53",
|
||||
"\u4f5c",
|
||||
"\u4f69",
|
||||
"\u4fa8",
|
||||
"\u4fdd",
|
||||
"\u4fe1",
|
||||
"\u4fee",
|
||||
"\u5012",
|
||||
"\u502a",
|
||||
"\u505a",
|
||||
"\u513f",
|
||||
"\u5149",
|
||||
"\u514b",
|
||||
"\u515c",
|
||||
"\u516c",
|
||||
"\u516d",
|
||||
"\u5170",
|
||||
"\u5171",
|
||||
"\u5177",
|
||||
"\u5185",
|
||||
"\u5199",
|
||||
"\u51b0",
|
||||
"\u51bb",
|
||||
"\u51c0",
|
||||
"\u51c9",
|
||||
"\u51e4",
|
||||
"\u51fa",
|
||||
"\u5206",
|
||||
"\u5207",
|
||||
"\u5229",
|
||||
"\u522b",
|
||||
"\u522e",
|
||||
"\u522e\u522e\u819c\u5c3a\u5bf8",
|
||||
"\u5230",
|
||||
"\u5236",
|
||||
"\u5237",
|
||||
"\u523b",
|
||||
"\u529b",
|
||||
"\u52a0",
|
||||
"\u52a0\u6025",
|
||||
"\u52a8",
|
||||
"\u5305",
|
||||
"\u5316",
|
||||
"\u5317",
|
||||
"\u533a",
|
||||
"\u533b",
|
||||
"\u5341",
|
||||
"\u5343",
|
||||
"\u534a",
|
||||
"\u534e",
|
||||
"\u5355",
|
||||
"\u5355\u53f7",
|
||||
"\u5357",
|
||||
"\u5361",
|
||||
"\u5370",
|
||||
"\u5382",
|
||||
"\u538b",
|
||||
"\u5398",
|
||||
"\u53a8",
|
||||
"\u53cc",
|
||||
"\u53d1",
|
||||
"\u53e4",
|
||||
"\u53eb",
|
||||
"\u53f6",
|
||||
"\u53f7",
|
||||
"\u53f8",
|
||||
"\u53fb",
|
||||
"\u5403",
|
||||
"\u5404",
|
||||
"\u5408",
|
||||
"\u5409",
|
||||
"\u540c",
|
||||
"\u540d",
|
||||
"\u5416",
|
||||
"\u5446",
|
||||
"\u548c",
|
||||
"\u5495",
|
||||
"\u5496",
|
||||
"\u54aa",
|
||||
"\u54c1",
|
||||
"\u54c6",
|
||||
"\u54c8",
|
||||
"\u54d1",
|
||||
"\u554a",
|
||||
"\u5561",
|
||||
"\u5566",
|
||||
"\u5584",
|
||||
"\u55b7",
|
||||
"\u563f",
|
||||
"\u565c",
|
||||
"\u56db",
|
||||
"\u56de",
|
||||
"\u56e1",
|
||||
"\u56e2",
|
||||
"\u56ed",
|
||||
"\u56fd",
|
||||
"\u56fe",
|
||||
"\u5706",
|
||||
"\u5728",
|
||||
"\u5730",
|
||||
"\u573a",
|
||||
"\u5740",
|
||||
"\u574a",
|
||||
"\u574f",
|
||||
"\u5766",
|
||||
"\u579a",
|
||||
"\u57ce",
|
||||
"\u57fa",
|
||||
"\u58ee",
|
||||
"\u58f0",
|
||||
"\u58f3",
|
||||
"\u58f9",
|
||||
"\u5904",
|
||||
"\u5907",
|
||||
"\u590d",
|
||||
"\u590f",
|
||||
"\u5915",
|
||||
"\u591a",
|
||||
"\u5927",
|
||||
"\u5929",
|
||||
"\u5934",
|
||||
"\u5947",
|
||||
"\u5948",
|
||||
"\u5956",
|
||||
"\u5957",
|
||||
"\u5973",
|
||||
"\u597d",
|
||||
"\u5986",
|
||||
"\u5988",
|
||||
"\u598d",
|
||||
"\u5999",
|
||||
"\u59ae",
|
||||
"\u59d0",
|
||||
"\u59d1",
|
||||
"\u59ec",
|
||||
"\u59ff",
|
||||
"\u5a18",
|
||||
"\u5a1c",
|
||||
"\u5a1f",
|
||||
"\u5b50",
|
||||
"\u5b54",
|
||||
"\u5b57",
|
||||
"\u5b5f",
|
||||
"\u5b64",
|
||||
"\u5b66",
|
||||
"\u5b69",
|
||||
"\u5b81",
|
||||
"\u5b85",
|
||||
"\u5b87",
|
||||
"\u5b89",
|
||||
"\u5b8b",
|
||||
"\u5b9a",
|
||||
"\u5b9d",
|
||||
"\u5ba2",
|
||||
"\u5bb0",
|
||||
"\u5bb6",
|
||||
"\u5bb9",
|
||||
"\u5bbe",
|
||||
"\u5bc4",
|
||||
"\u5bf8",
|
||||
"\u5c01",
|
||||
"\u5c0f",
|
||||
"\u5c1d",
|
||||
"\u5c27",
|
||||
"\u5c3a",
|
||||
"\u5c3a\u5bf8",
|
||||
"\u5c3c",
|
||||
"\u5c71",
|
||||
"\u5c81",
|
||||
"\u5c9b",
|
||||
"\u5ddd",
|
||||
"\u5dde",
|
||||
"\u5de5",
|
||||
"\u5de5\u827a",
|
||||
"\u5de6",
|
||||
"\u5deb",
|
||||
"\u5dee",
|
||||
"\u5df1",
|
||||
"\u5e02",
|
||||
"\u5e03",
|
||||
"\u5e06",
|
||||
"\u5e08",
|
||||
"\u5e0c",
|
||||
"\u5e1c",
|
||||
"\u5e45",
|
||||
"\u5e54",
|
||||
"\u5e72",
|
||||
"\u5e73",
|
||||
"\u5e74",
|
||||
"\u5e7f",
|
||||
"\u5e86",
|
||||
"\u5e8f",
|
||||
"\u5e95",
|
||||
"\u5ea6",
|
||||
"\u5efa",
|
||||
"\u5f04",
|
||||
"\u5f0f",
|
||||
"\u5f20",
|
||||
"\u5f39",
|
||||
"\u5f69",
|
||||
"\u5f71",
|
||||
"\u5f84",
|
||||
"\u5fbd",
|
||||
"\u5fc3",
|
||||
"\u5fc6",
|
||||
"\u601d",
|
||||
"\u6021",
|
||||
"\u6025",
|
||||
"\u6027",
|
||||
"\u602a",
|
||||
"\u603b",
|
||||
"\u610f",
|
||||
"\u61a8",
|
||||
"\u6210",
|
||||
"\u6237",
|
||||
"\u624b",
|
||||
"\u6253",
|
||||
"\u6263",
|
||||
"\u627e",
|
||||
"\u627f",
|
||||
"\u62a4",
|
||||
"\u62c9",
|
||||
"\u62fe",
|
||||
"\u6302",
|
||||
"\u6309",
|
||||
"\u6392",
|
||||
"\u6446",
|
||||
"\u644a",
|
||||
"\u6495",
|
||||
"\u6539",
|
||||
"\u653b",
|
||||
"\u653e",
|
||||
"\u653f",
|
||||
"\u654f",
|
||||
"\u6551",
|
||||
"\u6570",
|
||||
"\u6570\u91cf",
|
||||
"\u6587",
|
||||
"\u658c",
|
||||
"\u6599",
|
||||
"\u65a4",
|
||||
"\u65b9",
|
||||
"\u65cf",
|
||||
"\u65d7",
|
||||
"\u65e0",
|
||||
"\u65e5",
|
||||
"\u65e9",
|
||||
"\u65f6",
|
||||
"\u660e",
|
||||
"\u6613",
|
||||
"\u6615",
|
||||
"\u661f",
|
||||
"\u662f",
|
||||
"\u6643",
|
||||
"\u6653",
|
||||
"\u665a",
|
||||
"\u6668",
|
||||
"\u667a",
|
||||
"\u66f2",
|
||||
"\u6700",
|
||||
"\u6709",
|
||||
"\u6714",
|
||||
"\u671f",
|
||||
"\u6728",
|
||||
"\u6746",
|
||||
"\u674e",
|
||||
"\u6750\u8d28",
|
||||
"\u6761",
|
||||
"\u6770",
|
||||
"\u677f",
|
||||
"\u6797",
|
||||
"\u679c",
|
||||
"\u679d",
|
||||
"\u67ab",
|
||||
"\u67c4",
|
||||
"\u67d1",
|
||||
"\u67d2",
|
||||
"\u67d4",
|
||||
"\u6811",
|
||||
"\u6816",
|
||||
"\u6837",
|
||||
"\u6839",
|
||||
"\u683c",
|
||||
"\u6843",
|
||||
"\u6865",
|
||||
"\u6881",
|
||||
"\u6885",
|
||||
"\u6893",
|
||||
"\u68a6",
|
||||
"\u68cd",
|
||||
"\u6930",
|
||||
"\u697c",
|
||||
"\u6a21",
|
||||
"\u6a58",
|
||||
"\u6a59",
|
||||
"\u6a80",
|
||||
"\u6b21",
|
||||
"\u6b23",
|
||||
"\u6b27",
|
||||
"\u6b3e",
|
||||
"\u6bcf",
|
||||
"\u6c34",
|
||||
"\u6c49",
|
||||
"\u6c5f",
|
||||
"\u6c7d",
|
||||
"\u6c81",
|
||||
"\u6cb3",
|
||||
"\u6cbb",
|
||||
"\u6ce1",
|
||||
"\u6ce2",
|
||||
"\u6ce8",
|
||||
"\u6cea",
|
||||
"\u6cf0",
|
||||
"\u6d01",
|
||||
"\u6d12",
|
||||
"\u6d25",
|
||||
"\u6d41",
|
||||
"\u6d59",
|
||||
"\u6d77",
|
||||
"\u6db5",
|
||||
"\u6dcb",
|
||||
"\u6df7",
|
||||
"\u6dfc",
|
||||
"\u6e14",
|
||||
"\u6e21",
|
||||
"\u6e2f",
|
||||
"\u6e56",
|
||||
"\u6e58",
|
||||
"\u6e7e",
|
||||
"\u6e90",
|
||||
"\u6f47",
|
||||
"\u706b",
|
||||
"\u7070",
|
||||
"\u7075",
|
||||
"\u70ad",
|
||||
"\u70ae",
|
||||
"\u70b9",
|
||||
"\u70df",
|
||||
"\u70eb",
|
||||
"\u7167",
|
||||
"\u718a",
|
||||
"\u7231",
|
||||
"\u7247",
|
||||
"\u7248",
|
||||
"\u7259",
|
||||
"\u725b",
|
||||
"\u7269",
|
||||
"\u7279",
|
||||
"\u72ec",
|
||||
"\u732b",
|
||||
"\u738b",
|
||||
"\u73ab",
|
||||
"\u73b0",
|
||||
"\u73b2",
|
||||
"\u73e0",
|
||||
"\u73ed",
|
||||
"\u7433",
|
||||
"\u7470",
|
||||
"\u74dc",
|
||||
"\u7518",
|
||||
"\u751c",
|
||||
"\u751f",
|
||||
"\u7528",
|
||||
"\u7528\u6237\u4fe1\u606f",
|
||||
"\u75d5",
|
||||
"\u767d",
|
||||
"\u767e",
|
||||
"\u7684",
|
||||
"\u76ae",
|
||||
"\u76f4",
|
||||
"\u76f8",
|
||||
"\u7701",
|
||||
"\u77e5",
|
||||
"\u77f3",
|
||||
"\u7801",
|
||||
"\u7802",
|
||||
"\u786c",
|
||||
"\u7941",
|
||||
"\u798f",
|
||||
"\u79be",
|
||||
"\u79cb",
|
||||
"\u79cd",
|
||||
"\u7a7a",
|
||||
"\u7a7f",
|
||||
"\u7a9d",
|
||||
"\u7ae0",
|
||||
"\u7aef",
|
||||
"\u7b19",
|
||||
"\u7b2c",
|
||||
"\u7b3a",
|
||||
"\u7b52",
|
||||
"\u7b7e",
|
||||
"\u7bb1",
|
||||
"\u7c73",
|
||||
"\u7c91",
|
||||
"\u7c98",
|
||||
"\u7ca5",
|
||||
"\u7ccd",
|
||||
"\u7cd6",
|
||||
"\u7d2b",
|
||||
"\u7ea2",
|
||||
"\u7eb1",
|
||||
"\u7eb8",
|
||||
"\u7eb9",
|
||||
"\u7ebf",
|
||||
"\u7ec4",
|
||||
"\u7ecf",
|
||||
"\u7edf",
|
||||
"\u7ef3",
|
||||
"\u7eff",
|
||||
"\u7f0e",
|
||||
"\u7f1d",
|
||||
"\u7f29",
|
||||
"\u7f2a",
|
||||
"\u7f51",
|
||||
"\u7f8e",
|
||||
"\u7fbd",
|
||||
"\u7fca",
|
||||
"\u7fd4",
|
||||
"\u8001",
|
||||
"\u8010",
|
||||
"\u803f",
|
||||
"\u8083",
|
||||
"\u80e1",
|
||||
"\u80f6",
|
||||
"\u80fd",
|
||||
"\u8111",
|
||||
"\u819c",
|
||||
"\u81ea",
|
||||
"\u81f4",
|
||||
"\u820d",
|
||||
"\u8239",
|
||||
"\u8272",
|
||||
"\u827a",
|
||||
"\u827e",
|
||||
"\u8299",
|
||||
"\u82ad",
|
||||
"\u82b1",
|
||||
"\u82cf",
|
||||
"\u82f1",
|
||||
"\u8303",
|
||||
"\u8304",
|
||||
"\u8309",
|
||||
"\u831c",
|
||||
"\u8336",
|
||||
"\u8339",
|
||||
"\u8349",
|
||||
"\u8363",
|
||||
"\u8389",
|
||||
"\u8393",
|
||||
"\u83c7",
|
||||
"\u83dc",
|
||||
"\u840c",
|
||||
"\u8425",
|
||||
"\u8431",
|
||||
"\u843d",
|
||||
"\u8499",
|
||||
"\u84c9",
|
||||
"\u84dd",
|
||||
"\u85af",
|
||||
"\u85cf",
|
||||
"\u86cb",
|
||||
"\u86d9",
|
||||
"\u874e",
|
||||
"\u884c",
|
||||
"\u8865",
|
||||
"\u88c1",
|
||||
"\u88c5",
|
||||
"\u897f",
|
||||
"\u8981",
|
||||
"\u8986",
|
||||
"\u89d2",
|
||||
"\u8ba1",
|
||||
"\u8bb8",
|
||||
"\u8bbe",
|
||||
"\u8bfa",
|
||||
"\u8d21",
|
||||
"\u8d22",
|
||||
"\u8d27",
|
||||
"\u8d28",
|
||||
"\u8d34",
|
||||
"\u8d35",
|
||||
"\u8d70",
|
||||
"\u8d77",
|
||||
"\u8d85",
|
||||
"\u8f66",
|
||||
"\u8f6c",
|
||||
"\u8fb9",
|
||||
"\u8fbd",
|
||||
"\u8fce",
|
||||
"\u8fd9",
|
||||
"\u8fdc",
|
||||
"\u8ff7",
|
||||
"\u9001",
|
||||
"\u900f",
|
||||
"\u9053",
|
||||
"\u90c1",
|
||||
"\u914d",
|
||||
"\u9152",
|
||||
"\u9171",
|
||||
"\u9192",
|
||||
"\u91ca",
|
||||
"\u91cc",
|
||||
"\u91cd",
|
||||
"\u91cf",
|
||||
"\u91d1",
|
||||
"\u946b",
|
||||
"\u94b0",
|
||||
"\u94dc",
|
||||
"\u94ed",
|
||||
"\u94f6",
|
||||
"\u9542",
|
||||
"\u95f4",
|
||||
"\u9632",
|
||||
"\u9633",
|
||||
"\u963f",
|
||||
"\u9648",
|
||||
"\u9655",
|
||||
"\u9675",
|
||||
"\u96c5",
|
||||
"\u96e8",
|
||||
"\u96ea",
|
||||
"\u96f6",
|
||||
"\u9732",
|
||||
"\u9752",
|
||||
"\u9762",
|
||||
"\u9879",
|
||||
"\u987a",
|
||||
"\u9891",
|
||||
"\u9897",
|
||||
"\u9898",
|
||||
"\u989c",
|
||||
"\u98ce",
|
||||
"\u98de",
|
||||
"\u98df",
|
||||
"\u996d",
|
||||
"\u9970",
|
||||
"\u9986",
|
||||
"\u9999",
|
||||
"\u9a74",
|
||||
"\u9ad8",
|
||||
"\u9b3c",
|
||||
"\u9c7c",
|
||||
"\u9e1f",
|
||||
"\u9e25",
|
||||
"\u9e45",
|
||||
"\u9e7f",
|
||||
"\u9ea6",
|
||||
"\u9ebb",
|
||||
"\u9ecf",
|
||||
"\u9ed1",
|
||||
"\u9f99",
|
||||
"\u9f9f",
|
||||
"\uff08",
|
||||
"\uff09",
|
||||
"\uff0c",
|
||||
"\uff1a",
|
||||
"\uff1b"
|
||||
]
|
||||
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"mode":"default"
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
[paths]
|
||||
train = null
|
||||
dev = null
|
||||
vectors = null
|
||||
init_tok2vec = null
|
||||
|
||||
[system]
|
||||
seed = 0
|
||||
gpu_allocator = null
|
||||
|
||||
[nlp]
|
||||
lang = "zh"
|
||||
pipeline = ["ner"]
|
||||
disabled = []
|
||||
before_creation = null
|
||||
after_creation = null
|
||||
after_pipeline_creation = null
|
||||
batch_size = 1000
|
||||
vectors = {"@vectors":"spacy.Vectors.v1"}
|
||||
|
||||
[nlp.tokenizer]
|
||||
@tokenizers = "spacy.zh.ChineseTokenizer"
|
||||
segmenter = "char"
|
||||
|
||||
[components]
|
||||
|
||||
[components.ner]
|
||||
factory = "ner"
|
||||
incorrect_spans_key = null
|
||||
moves = null
|
||||
scorer = {"@scorers":"spacy.ner_scorer.v1"}
|
||||
update_with_oracle_cut_size = 100
|
||||
|
||||
[components.ner.model]
|
||||
@architectures = "spacy.TransitionBasedParser.v2"
|
||||
state_type = "ner"
|
||||
extra_state_tokens = false
|
||||
hidden_width = 64
|
||||
maxout_pieces = 2
|
||||
use_upper = true
|
||||
nO = null
|
||||
|
||||
[components.ner.model.tok2vec]
|
||||
@architectures = "spacy.HashEmbedCNN.v2"
|
||||
pretrained_vectors = null
|
||||
width = 96
|
||||
depth = 4
|
||||
embed_size = 2000
|
||||
window_size = 1
|
||||
maxout_pieces = 3
|
||||
subword_features = true
|
||||
|
||||
[corpora]
|
||||
|
||||
[corpora.dev]
|
||||
@readers = "spacy.Corpus.v1"
|
||||
path = ${paths.dev}
|
||||
gold_preproc = false
|
||||
max_length = 0
|
||||
limit = 0
|
||||
augmenter = null
|
||||
|
||||
[corpora.train]
|
||||
@readers = "spacy.Corpus.v1"
|
||||
path = ${paths.train}
|
||||
gold_preproc = false
|
||||
max_length = 0
|
||||
limit = 0
|
||||
augmenter = null
|
||||
|
||||
[training]
|
||||
seed = ${system.seed}
|
||||
gpu_allocator = ${system.gpu_allocator}
|
||||
dropout = 0.1
|
||||
accumulate_gradient = 1
|
||||
patience = 1600
|
||||
max_epochs = 0
|
||||
max_steps = 20000
|
||||
eval_frequency = 200
|
||||
frozen_components = []
|
||||
annotating_components = []
|
||||
dev_corpus = "corpora.dev"
|
||||
train_corpus = "corpora.train"
|
||||
before_to_disk = null
|
||||
before_update = null
|
||||
|
||||
[training.batcher]
|
||||
@batchers = "spacy.batch_by_words.v1"
|
||||
discard_oversize = false
|
||||
tolerance = 0.2
|
||||
get_length = null
|
||||
|
||||
[training.batcher.size]
|
||||
@schedules = "compounding.v1"
|
||||
start = 100
|
||||
stop = 1000
|
||||
compound = 1.001
|
||||
t = 0.0
|
||||
|
||||
[training.logger]
|
||||
@loggers = "spacy.ConsoleLogger.v1"
|
||||
progress_bar = false
|
||||
|
||||
[training.optimizer]
|
||||
@optimizers = "Adam.v1"
|
||||
beta1 = 0.9
|
||||
beta2 = 0.999
|
||||
L2_is_weight_decay = true
|
||||
L2 = 0.01
|
||||
grad_clip = 1.0
|
||||
use_averages = false
|
||||
eps = 0.00000001
|
||||
learn_rate = 0.001
|
||||
|
||||
[training.score_weights]
|
||||
ents_f = 1.0
|
||||
ents_p = 0.0
|
||||
ents_r = 0.0
|
||||
ents_per_type = null
|
||||
|
||||
[pretraining]
|
||||
|
||||
[initialize]
|
||||
vectors = ${paths.vectors}
|
||||
init_tok2vec = ${paths.init_tok2vec}
|
||||
vocab_data = null
|
||||
lookups = null
|
||||
before_init = null
|
||||
after_init = null
|
||||
|
||||
[initialize.components]
|
||||
|
||||
[initialize.tokenizer]
|
||||
pkuseg_model = null
|
||||
pkuseg_user_dict = "default"
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"lang":"zh",
|
||||
"name":"pipeline",
|
||||
"version":"0.0.0",
|
||||
"spacy_version":">=3.8.7,<3.9.0",
|
||||
"description":"",
|
||||
"author":"",
|
||||
"email":"",
|
||||
"url":"",
|
||||
"license":"",
|
||||
"spacy_git_version":"4b65aa7",
|
||||
"vectors":{
|
||||
"width":0,
|
||||
"vectors":0,
|
||||
"keys":0,
|
||||
"name":null,
|
||||
"mode":"default"
|
||||
},
|
||||
"labels":{
|
||||
"ner":[
|
||||
"\u522e\u522e\u819c\u5c3a\u5bf8",
|
||||
"\u5c3a\u5bf8",
|
||||
"\u6570\u91cf"
|
||||
]
|
||||
},
|
||||
"pipeline":[
|
||||
"ner"
|
||||
],
|
||||
"components":[
|
||||
"ner"
|
||||
],
|
||||
"disabled":[
|
||||
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"moves":null,
|
||||
"update_with_oracle_cut_size":100,
|
||||
"multitasks":[
|
||||
|
||||
],
|
||||
"min_action_freq":1,
|
||||
"learn_tokens":false,
|
||||
"beam_width":1,
|
||||
"beam_density":0.0,
|
||||
"beam_update_prob":0.0,
|
||||
"incorrect_spans_key":null
|
||||
}
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user