chore: initial commit
Some checks failed
Synchronize to Gitee / repo-sync (push) Has been cancelled
Typos Checking / Spell Check with Typos (push) Has been cancelled

This commit is contained in:
2026-06-23 11:56:23 +08:00
commit 72e2110987
2883 changed files with 367388 additions and 0 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
**/node_modules/
**/.node/
**/dist/
**/pnpm-lock.yaml

71
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
name: BUG 提交
description: 提交产品缺陷帮助我们更好的改进
title: "[BUG] "
labels: ["bug"]
assignees: ["zrfit"]
body:
- type: markdown
id: contacts_title
attributes:
value: "## 联系方式"
- type: input
id: contacts
validations:
required: false
attributes:
label: "联系方式"
description: "可以快速联系到您进一步沟通的方式:交流群号及昵称、邮箱等"
- type: markdown
id: environment
attributes:
value: "## 环境信息"
- type: input
id: version
validations:
required: true
attributes:
label: "Cordys CRM 版本"
description: "在系统右上角的下拉菜单中选择 `关于` 可查看当前 Cordys CRM 版本。"
- type: dropdown
id: database
validations:
required: true
attributes:
label: "使用外置数据库"
description: "如果使用外置数据库,请确认您的外置数据库版本,目前仅支持 MySQL 8.x 与 MariaDB 11.x。"
options:
-
-
- type: markdown
id: details
attributes:
value: "## 详细信息"
- type: textarea
id: what-happened
attributes:
label: "问题描述"
description: "简要描述您碰到的问题"
validations:
required: true
- type: textarea
id: how-happened
attributes:
label: "重现步骤"
description: "如果操作可以重现该问题"
validations:
required: true
- type: textarea
id: expect
attributes:
label: "期待的正确结果"
- type: textarea
id: logs
attributes:
label: "相关日志输出"
description: "请复制并粘贴任何相关的日志输出。 这将自动格式化为代码,因此无需反引号。"
render: shell
- type: textarea
id: additional-information
attributes:
label: "附加信息"
description: "如果你还有其他需要提供的信息,可以在这里填写(可以提供截图、视频等)。"

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 对 Cordys CRM 项目有其他问题
url: https://github.com/1Panel-dev/CordysCRM/
about: 如果想要进一步了解 Cordys CRM 项目,欢迎发送邮件到 support@fit2cloud.com 进行提问。
- name: 反馈安全问题 (Report Security Bug)
url: mailto://support@fit2cloud.com
about: 请通过邮箱 support@fit2cloud.com 反馈安全问题 (Please report security vulnerabilities to support@fit2cloud.com)

43
.github/ISSUE_TEMPLATE/feature.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: 需求建议
description: 提出针对本项目的想法和建议
title: "[FEATURE] "
labels: ["需求"]
assignees: ["luty2018", "sakura1412"]
body:
- type: markdown
id: environment
attributes:
value: "## 环境信息"
- type: input
id: version
validations:
required: true
attributes:
label: "Cordys CRM 版本"
description: "在系统右上角的下拉菜单中选择 `关于` 可查看当前 Cordys CRM 版本。"
- type: input
id: contacts
validations:
required: false
attributes:
label: "联系方式"
description: "可以快速联系到您进一步沟通的方式:交流群号及昵称、邮箱等"
- type: markdown
id: details
attributes:
value: "## 详细信息"
- type: textarea
id: description
attributes:
label: "请描述您的需求或者改进建议"
validations:
required: true
- type: textarea
id: solution
attributes:
label: "请描述你建议的实现方案"
- type: textarea
id: additional-information
attributes:
label: "附加信息"
description: "如果你还有其他需要提供的信息,可以在这里填写(可以提供截图、视频等)。"

12
.github/ISSUE_TEMPLATE/question.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: 问题咨询
description: 提出针对本项目安装部署、使用及其他方面的相关问题
title: "[QUESTION] "
labels: ["question"]
assignees: ["luty2018", "sakura1412"]
body:
- type: textarea
id: description
attributes:
label: "请描述您的问题"
validations:
required: true

9
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,9 @@
#### What this PR does / why we need it?
#### Summary of your change
#### Please indicate you've done the following:
- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.

38
.github/workflows/add-labels-for-pr.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
on:
workflow_dispatch:
name: Add Labels to PR
jobs:
add_labels:
runs-on: ubuntu-latest
steps:
# Checkout the repository code
- name: Checkout code
uses: actions/checkout@v3
# Extract and format the labels from the comment as "username: label"
- name: Extract reviewer and labels
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/') }}
id: extract_labels
run: |
comment_body="${{ github.event.comment.body }}"
reviewer="${{ github.event.comment.user.login }}"
# Clean up comment and extract labels
raw_labels=$(echo "$comment_body" | sed 's|^/||')
formatted_labels=$(echo "$raw_labels" | awk -v reviewer="$reviewer" '{split($0, arr, ","); for (i in arr) {gsub(/^ +| +$/, "", arr[i]); print reviewer ": " arr[i]}}')
# Set the formatted labels as an environment variable (new way, not using set-output)
echo "labels=$formatted_labels" >> $GITHUB_ENV
# Output labels for debugging
echo "Formatted labels: $formatted_labels"
# Add the extracted labels to the PR
- name: Add labels via PR comment
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/') }}
uses: actions-ecosystem/action-add-labels@v1
with:
github_token: ${{ secrets.GH_TOKEN }}
labels: ${{ env.labels }}

View File

@@ -0,0 +1,52 @@
name: build-and-push-base
on:
workflow_dispatch:
inputs:
architecture:
description: 'Architecture'
required: true
default: 'linux/amd64'
type: choice
options:
- linux/amd64
- linux/arm64
- linux/amd64,linux/arm64
jobs:
build-and-push-base-to-ghcr:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: main
- name: Prepare
id: prepare
run: |
DOCKER_IMAGE=ghcr.io/cordys-dev/cordys-base
DOCKER_PLATFORMS=${{ github.event.inputs.architecture }}
TAG_NAME=latest
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:latest"
echo ::set-output name=docker_image::${DOCKER_IMAGE}
echo ::set-output name=version::${TAG_NAME}
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} --no-cache \
--build-arg VERSION=${TAG_NAME} \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg VCS_REF=${GITHUB_SHA::8} \
${DOCKER_IMAGE_TAGS} .
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GH_TOKEN }}
- name: Docker Buildx (build-and-push)
run: |
docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} -f installer/Dockerfile.base

108
.github/workflows/build-and-push.yml vendored Normal file
View File

@@ -0,0 +1,108 @@
name: build-and-push
run-name: 构建镜像并推送仓库 ${{ github.event.inputs.dockerImageTag }} (${{ github.event.inputs.registry }})
on:
workflow_dispatch:
inputs:
dockerImageTag:
description: 'Docker Image Tag'
default: 'dev'
required: true
architecture:
description: 'Architecture'
required: true
default: 'linux/amd64'
type: choice
options:
- linux/amd64
- linux/arm64
- linux/amd64,linux/arm64
registry:
description: 'Push To Registry'
required: true
default: 'fit2cloud-registry'
type: choice
options:
- fit2cloud-registry
jobs:
build-and-push-to-fit2cloud-registry:
if: ${{ contains(github.event.inputs.registry, 'fit2cloud') }}
runs-on: ubuntu-latest
steps:
- name: Checkout specific repository
uses: actions/checkout@v3
with:
repository: cordys-dev/cordys-crm
token: ${{ secrets.GH_TOKEN }} # 使用 GitHub token
- name: Prepare
id: prepare
run: |
DOCKER_IMAGE=${{ secrets.FIT2CLOUD_REGISTRY_HOST }}/cordys/cordys-crm-ce
DOCKER_PLATFORMS=${{ github.event.inputs.architecture }}
TAG_NAME=${{ github.event.inputs.dockerImageTag }}
SHORT_SHA=$(git rev-parse --short HEAD)
if [[ ${TAG_NAME} == *dev* ]]; then
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME}"
else
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:latest"
fi
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \
--build-arg DOCKER_IMAGE_TAG=${{ github.event.inputs.dockerImageTag }} --build-arg BUILD_AT=$(TZ=Asia/Shanghai date +'%Y-%m-%dT%H:%M') --build-arg GITHUB_COMMIT=${GITHUB_SHA::8} --no-cache \
--build-arg FIT2CLOUD_MAVEN_USERNAME=${{ secrets.FIT2CLOUD_MAVEN_USERNAME }} --build-arg FIT2CLOUD_MAVEN_PASSWORD=${{ secrets.FIT2CLOUD_MAVEN_PASSWORD }} \
--build-arg CRM_VERSION=${TAG_NAME}-${SHORT_SHA} \
${DOCKER_IMAGE_TAGS} .
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to FIT2CLOUD Registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.FIT2CLOUD_REGISTRY_HOST }}
username: ${{ secrets.FIT2CLOUD_REGISTRY_USERNAME }}
password: ${{ secrets.FIT2CLOUD_REGISTRY_PASSWORD }}
- name: Docker Buildx (build-and-push)
run: |
docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} -f installer/Dockerfile
# 创建或更新标签
- name: Create or Re-create Tag
if: success()
run: |
git config --global user.name 'fit2-zhao'
git config --global user.email 'yong.zhao@fit2cloud.com'
# 判断是否为dev标签如果是则跳过打tag操作
if [[ "${{ github.event.inputs.dockerImageTag }}" == "dev" ]]; then
echo "当前为dev标签跳过打tag操作"
exit 0
fi
# 获取远程标签信息
git fetch --prune --tags
# 检查标签是否存在(本地或远程)
if git ls-remote --tags origin refs/tags/${{ github.event.inputs.dockerImageTag }} | grep -q "${{ github.event.inputs.dockerImageTag }}" || git tag -l "${{ github.event.inputs.dockerImageTag }}" | grep -q "${{ github.event.inputs.dockerImageTag }}"; then
echo "标签 ${{ github.event.inputs.dockerImageTag }} 已存在,正在删除..."
# 删除本地标签(如果存在)
git tag -d ${{ github.event.inputs.dockerImageTag }} || true
# 删除远程标签并确认结果
git push --delete origin ${{ github.event.inputs.dockerImageTag }} || echo "远程标签可能不存在或已被删除"
# 确认标签已被删除
sleep 2
git fetch --prune --tags
fi
# 创建和推送新标签
git tag -a ${{ github.event.inputs.dockerImageTag }} -m "Release ${{ github.event.inputs.dockerImageTag }}"
git push origin ${{ github.event.inputs.dockerImageTag }}

46
.github/workflows/codecov.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
on:
pull_request:
branches:
- main
paths:
- "backend/**"
- "pom.xml"
name: Code Coverage
permissions:
pull-requests: write
jobs:
generic_handler:
name: Code Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
token: ${{ secrets.GH_TOKEN }}
# Cache Maven dependencies
- name: Cache Maven dependencies
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('backend/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 21
check-latest: false
- name: Build with Maven - other
run: mvn -B package -DskipAntRunForJenkins --file pom.xml -pl '!frontend'
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: 1Panel-dev/CordysCRM

View File

@@ -0,0 +1,17 @@
on:
push:
branches:
- 'pr@**'
- 'repr@**'
name: Auto Create PR
jobs:
generic_handler:
name: Auto Create PR
runs-on: ubuntu-latest
steps:
- name: Create pull request
uses: jumpserver/action-generic-handler@master
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}

30
.github/workflows/frontend-build.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
on:
pull_request:
branches:
- main
paths:
- "frontend/**"
name: Frontend Code Checking
permissions:
pull-requests: write
jobs:
generic_handler:
name: Frontend Code Checking
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
token: ${{ secrets.GH_TOKEN }}
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 21
cache: 'maven'
cache-dependency-path: '**/pom.xml' # optional
check-latest: true
- name: Build with Maven - only frontend
run: mvn -T 1C -B package --file pom.xml -pl frontend

16
.github/workflows/issue-close.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Issue Close Check
on:
issues:
types: [closed]
jobs:
issue-close-remove-labels:
runs-on: ubuntu-latest
steps:
- name: Remove labels
uses: actions-cool/issues-helper@v2
if: ${{ !github.event.issue.pull_request }}
with:
actions: 'remove-labels'
labels: '待处理'

38
.github/workflows/issue-comment.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
on:
issue_comment:
types: [created]
name: Add issues workflow labels
jobs:
add-label-if-is-author:
runs-on: ubuntu-latest
if: ${{ (github.event.issue.user.id == github.event.comment.user.id) && (!github.event.issue.pull_request) }}
steps:
- name: Add require handle label
uses: actions-cool/issues-helper@v2
with:
actions: 'add-labels'
labels: '待处理'
- name: Remove require reply label
uses: actions-cool/issues-helper@v2
with:
actions: 'remove-labels'
labels: '待用户反馈'
add-label-if-not-author:
runs-on: ubuntu-latest
if: ${{ (github.event.issue.user.id != github.event.comment.user.id) && (!github.event.issue.pull_request) && (github.event.issue.state == 'open') }}
steps:
- name: Add require replay label
uses: actions-cool/issues-helper@v2
with:
actions: 'add-labels'
labels: '待用户反馈'
- name: Remove require handle label
uses: actions-cool/issues-helper@v2
with:
actions: 'remove-labels'
labels: '待处理'

16
.github/workflows/issue-open.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Issue Open Check
on:
issues:
types: [opened]
jobs:
issue-open-add-labels:
runs-on: ubuntu-latest
steps:
- name: Add labels
uses: actions-cool/issues-helper@v2
if: ${{ !github.event.issue.pull_request }}
with:
actions: 'add-labels'
labels: '待处理'

109
.github/workflows/issue-sync-to-tapd.yml vendored Normal file
View File

@@ -0,0 +1,109 @@
name: Sync GitHub Issues to TAPD
on:
issues:
types:
- assigned
- reopened
permissions:
issues: write
jobs:
sync-to-tapd:
runs-on: ubuntu-latest
concurrency:
group: sync-issue-${{ github.event.issue.number }}
cancel-in-progress: false
steps:
- name: Check if sync is needed (assignee + live labels)
id: check
run: |
ASSIGNEE="${{ github.event.issue.assignee.login }}"
# Fetch live labels from GitHub API (not the snapshot in the event)
LABELS=$(gh issue view ${{ github.event.issue.number }} \
--repo ${{ github.repository }} \
--json labels \
--jq '.labels[].name')
if [ -z "$ASSIGNEE" ] || [ "$ASSIGNEE" != "luty2018" ]; then
echo "Issue is not assigned to luty2018 or has no assignee, skipping sync"
echo "should_sync=false" >> $GITHUB_OUTPUT
elif echo "$LABELS" | grep -qx "tapd-synced"; then
echo "Issue already has the tapd-synced label, already synced, skipping"
echo "should_sync=false" >> $GITHUB_OUTPUT
else
echo "Issue is assigned to luty2018 and not yet synced, proceeding to sync"
echo "should_sync=true" >> $GITHUB_OUTPUT
fi
env:
GH_TOKEN: ${{ github.token }}
- name: Sync to TAPD
if: steps.check.outputs.should_sync == 'true'
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_URL: ${{ github.event.issue.html_url }}
ISSUE_BODY: ${{ github.event.issue.body }}
TAPD_WORKSPACE_ID: ${{ secrets.TAPD_WORKSPACE_ID }}
TAPD_PROJECT_ID: ${{ secrets.TAPD_PROJECT_ID }}
TAPD_ITERATION_ID: ${{ secrets.TAPD_ITERATION_ID }}
TAPD_API_USERNAME: ${{ secrets.TAPD_API_USERNAME }}
TAPD_API_PASSWORD: ${{ secrets.TAPD_API_PASSWORD }}
TAPD_API_OWNER: ${{ secrets.TAPD_API_OWNER }}
run: |
# Fallback for empty body
BODY="${ISSUE_BODY:-No description}"
TITLE="[GitHub Issue #${ISSUE_NUMBER}] ${ISSUE_TITLE}"
BODY_HTML="${BODY//$'\n'/<br>}"
DESCRIPTION="GitHub link: ${ISSUE_URL}<br><br>Description: ${BODY_HTML}"
JSON=$(jq -n \
--arg name "$TITLE" \
--arg desc "$DESCRIPTION" \
--arg proj "$TAPD_PROJECT_ID" \
--arg ws "$TAPD_WORKSPACE_ID" \
--arg iteration "$TAPD_ITERATION_ID" \
--arg user "$TAPD_API_USERNAME" \
--arg owner "$TAPD_API_OWNER" \
'{
name: $name,
description: $desc,
project_id: $proj,
workspace_id: $ws,
owner: $owner,
creator: $owner
} + if $iteration != "" then {iteration_id: $iteration} else {} end')
echo "Creating TAPD story..."
echo "Title: $TITLE"
echo "Description: $DESCRIPTION"
HTTP_CODE=$(curl -s -o /dev/stderr -w "%{http_code}" -X POST \
"https://api.tapd.cn/stories?workspace_id=${TAPD_WORKSPACE_ID}" \
-H "Content-Type: application/json" \
-u "${TAPD_API_USERNAME}:${TAPD_API_PASSWORD}" \
-d "$JSON")
if [ "$HTTP_CODE" -ne 200 ] && [ "$HTTP_CODE" -ne 201 ]; then
echo "::error::TAPD API returned HTTP ${HTTP_CODE}, check the response details above"
exit 1
fi
echo "::notice::Sync completed, TAPD story created"
- name: Add synced label
if: steps.check.outputs.should_sync == 'true'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['tapd-synced']
})

41
.github/workflows/llm-code-review.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: LLM Code Review
permissions:
contents: write
pull-requests: write
on:
workflow_dispatch:
jobs:
llm-code-review:
runs-on: ubuntu-latest
steps:
# 1⃣ 拉代码
- name: Checkout code
uses: actions/checkout@v3
# 2⃣ LLM Code Review发表评论
- name: Run LLM Code Review
id: llm-code-review
uses: fit2cloud/LLM-CodeReview-Action@main
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
OPENAI_API_KEY: ${{ secrets.ALIYUN_LLM_API_KEY }}
OPENAI_API_ENDPOINT: https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL: qwen3-coder-plus
LANGUAGE: Chinese
PROMPT: |
你是资深代码审查专家,严格检查以下代码差异。
⚠️【必须遵守】
- 只在代码质量存在问题时发表评论。
- 不要解释、不添加多余文本。
- 审查应专注于以下内容:逻辑错误、代码规范、可读性、性能优化等。
temperature: 0.2
top_p: 1
MAX_PATCH_LENGTH: 50000
IGNORE_PATTERNS: "/node_modules,*.md,/dist,/.github"
FILE_PATTERNS: "*.java"

15
.github/workflows/sync2gitee.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Synchronize to Gitee
on: [push]
jobs:
repo-sync:
runs-on: ubuntu-latest
steps:
- name: Mirror the Github organization repos to Gitee.
uses: Yikun/hub-mirror-action@master
with:
src: 'github/1Panel-dev'
dst: 'gitee/fit2cloud-feizhiyun'
dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_TOKEN }}
static_list: "CordysCRM"
force_update: true

23
.github/workflows/typos-check.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Typos Checking
on:
push:
branches:
- '**'
pull_request:
types: [opened, synchronize, reopened]
permissions:
pull-requests: write
jobs:
run:
name: Spell Check with Typos
runs-on: ubuntu-latest
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v2
- name: Check spelling
uses: crate-ci/typos@master
with:
config: .github/workflows/typos-extend-exclude.toml

View File

@@ -0,0 +1,2 @@
[files]
extend-exclude = ["*.json", "**/*.json", "**/*.js", "**/*.html", "**/*.xml", "**/config.ts", "**/*.sql", "**/*.conf", "**/en-US.ts", "OWNERS", "CODEOWNERS", "LICENSE", "NOTICE"]

80
.gitignore vendored Normal file
View File

@@ -0,0 +1,80 @@
############################
# Environment & Config
############################
# Local env files
.env
.env.local
.env.*.local
.venv
# Maven
.mvn
!.mvn/maven-wrapper.properties
.flattened-pom.xml
############################
# Logs
############################
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-error.log*
############################
# IDE / Editor
############################
# macOS
.DS_Store
# IntelliJ IDEA
.idea/*
!.idea/icon.png
**/*.iml
# Eclipse
.settings
.project
.classpath
.factorypath
# VS Code
.vscode
# Visual Studio
*.suo
*.ntvs*
*.njsproj
*.sln
# Other
*.sw?
.history
############################
# Build Output
############################
target
.jython_cache
############################
# Static Resources (ignored)
############################
src/main/resources/static
src/main/resources/templates
/backend/crm/src/main/resources/static/
/backend/app/src/main/resources/static/
############################
# Custom Files
############################
qywx.json
.gitattributes
# Claude Code
.claude

12
.typos.toml Normal file
View File

@@ -0,0 +1,12 @@
[default.extend-words]
AKE = "AKE"
[default.extend-identifiers]
maintain_column_froms = "maintain_column_froms"
RegistCapi="RegistCapi"
[files]
extend-exclude = [
"frontend/public",
"frontend/packages/web/src/components/business/crm-city-select/config.ts",
"backend/crm/src/main/resources/region/region.json",
"iconfont.json"
]

49
BUILD.md Normal file
View File

@@ -0,0 +1,49 @@
# 构建过程说明
本项目包含多个模块,构建分为基础配置安装、后端构建、前端构建和整体打包四个部分。
---
## 🔧 1. 安装基础 POM
该命令会将 `parent pom` 安装到本地 Maven 仓库,使其他外部子工程可以获取最新的 `<properties>` 配置。
```bash
# 如果遇到网络问题,可以使用阿里云镜像加速
./mvnw install -N
```
这是多模块项目的必要步骤确保所有子模块能正确继承父POM的配置。
---
## 🖥️ 2. 后端构建
执行以下命令构建后端模块(如 `backend` 中的 `framework``crm``app` 等)并安装到本地仓库:
```bash
./mvnw clean install -DskipTests -DskipAntRunForJenkins --file backend/pom.xml
```
> ✅ 参数说明:
>
> * `-DskipTests`: 跳过测试用例执行。
> * `-DskipAntRunForJenkins`: 跳过 Jenkins 使用的 Ant 任务。
---
## 💻 3. 前端构建
前端构建请参考 [`/frontend/REDEME.md`](./frontend/REDEME.md) 中的具体说明!
> 📌 提示:确保运行环境已正确安装 Node.js 和依赖环境!
---
## 📦 4. 整体打包
使用以下命令进行完整的构建与打包:
```bash
./mvnw clean package
```
> ✅ 备注:
> mvnw 为项目自带打包工具,也可使用本地部署的 mvn

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
support@fit2cloud.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

38
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,38 @@
# Contributing
As a contributor, please acknowledge and agree that:
- The project maintainers may adjust the open-source license terms as necessary to make them either more strict or more relaxed.
- Any code you contribute may be used for commercial purposes, including but not limited to the projects cloud-based operations.
## Creating a Pull Request
Pull requests (PRs) are always welcome — even small ones that fix typos or minor issues.
If your contribution involves significant work or new features, please open an issue first to start a discussion before you begin implementation.
Please break down your changes into small, focused PRs whenever possible.
Large PRs with many features or extensive changes are difficult to review and may take longer to merge. Incremental submissions are strongly encouraged.
These development guidelines provide information about the repository structure, how to set up your development environment, how to run the project, and more.
**Note:** When splitting a feature into multiple PRs, please ensure that each individual PR can be safely merged into the `master` branch without breaking existing functionality.
If a PR introduces incomplete or non-functional changes, it will not be merged until the feature is complete.
## Reporting Issues
Reporting issues is a great way to contribute to the project.
Well-written and detailed bug reports are always appreciated! Please open a new issue and fill out all required information using the provided template.
Before opening a new issue, please check the existing issue list to avoid duplicates.
If a similar issue already exists, you can subscribe to it to receive updates.
If you have additional information to share, please add it as a comment on that issue.
When reporting an issue, please include the following information:
- The version you are using
- Steps to reproduce the problem
- Screenshots or log files, if applicable
Because all issues are public, please ensure that you remove any sensitive information (e.g., usernames, passwords, IP addresses, or company names) from your submissions.
Replace sensitive data with placeholders such as `REDACTED` or `****`.

18
LICENSE Normal file
View File

@@ -0,0 +1,18 @@
This project is distributed under the GNU General Public License, version 3 (GPLv3),
with the following additional terms and conditions:
1. Logos and Copyright
You may not remove, alter, or hide any logos, trademarks, or copyright
notices displayed in the web interface or within the source code.
2. Contributor Terms
By contributing to this project, you agree that:
a. The project maintainer may update or revise this open-source license
to make it more permissive or more restrictive as needed.
b. Your contributions may be used for commercial purposes, including
but not limited to the projects cloud and business operations.
All other terms of the GNU General Public License, version 3 (GPLv3) remain in effect.
Full text: https://www.gnu.org/licenses/gpl-3.0.html
Copyright (c) 2026-present FIT2CLOUD.

21
OWNERS Normal file
View File

@@ -0,0 +1,21 @@
reviewers:
- fit2-zhao
- xiaomeinvG
- song-tianyang
- AgAngle
- ba1q1
- song-cc-rock
- WangXu10
- fit2cloudwxx
- 1myuan
approvers:
- fit2-zhao
- xiaomeinvG
- song-tianyang
- AgAngle
- ba1q1
- song-cc-rock
- WangXu10
- fit2cloudwxx
- 1myuan

140
README.md Normal file
View File

@@ -0,0 +1,140 @@
<h1 align="center">Cordys CRM</h1>
<h3 align="center">新一代的开源 AI CRM 系统</h3>
<p align="center">
<a href="https://trendshift.io/repositories/15469" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15469" alt="1Panel-dev%2FCordysCRM | Trendshift" style="width: 240px; height: auto;" /></a>
</p>
<p align="center">
<a href="https://github.com/1Panel-dev/CordysCRM/releases"><img src="https://img.shields.io/github/v/release/1Panel-dev/CordysCRM" alt="Latest release"></a>
<a href="https://github.com/1Panel-dev/CordysCRM"><img src="https://img.shields.io/github/stars/1Panel-dev/CordysCRM?color=%231890FF&style=flat-square" alt="Stars"></a>
<a href="https://hub.docker.com/r/1panel/cordys-crm"><img src="https://img.shields.io/docker/pulls/1panel/cordys-crm?label=downloads" alt="Download"></a><br/>
</p>
<hr/>
## 什么是 Cordys CRM
**Cordys CRM** 是新一代的开源 AI CRM 系统,是集信息化、数字化、智能化于一体的「客户关系管理系统」,由 [飞致云](https://fit2cloud.com/) 匠心出品。
Cordys CRM 能够帮助企业实现从线索到回款L2C的全流程精细化管理覆盖线索获取、智能分配、客户与联系人管理、商机跟进、合同签约及回款执行构建端到端的销售运营闭环。
<img alt="Cordys CRM Overview" src="https://cordys.cn/images/cordyscrm-202606.png" />
## 核心优势
- **灵活配置 · 高效协同**:现代化架构,操作简洁流畅;精细权限与模块化配置,无缝集成主流办公平台,赋能团队高效协同;
- **安全自主 · 深度可控**为私有化部署而生数据100%自主可控开放API与标准接口支持深度集成与定制开发筑牢安全与发展基石
- **智能 BI · 决策赋能**:深度融合 DataEase 分析引擎,销售数据可视化呈现;支持自助探索与实时洞察,驱动精准决策与业绩增长;
- **AI 赋能 · 智能提效**:开放 CRM Skills 接口,让 AI 深入销售每个环节OpenClaw、WorkBuddy 等智能助手 7×24 小时在线,从线索筛选到成单分析,全流程提效。
## 快速开始
### 安装部署
准备一台 Linux 服务器,安装好 [Docker](https://docs.docker.com/get-docker/) 后,执行以下一键安装脚本。
```bash
docker run -d \
--name cordys-crm \
--restart unless-stopped \
-p 8081:8081 \
-p 8082:8082 \
-v ~/cordys:/opt/cordys \
1panel/cordys-crm
```
你也可以通过 [1Panel 应用商店](https://cordys.cn/docs/installation/1panel_installtion/) 来安装部署 Cordys CRM。
在无法联网的环境中,还可以通过 [离线安装包](https://cordys.cn/docs/installation/offline_installtion/) 来安装部署 Cordys CRM。
### 访问方式
- 在浏览器中打开: http://<你的服务器IP>:8081/
- 用户名: `admin`
- 密码: `CordysCRM`
### 联系我们
安装完成后,可以参考 [在线文档](https://cordys.cn/docs/) 来使用 Cordys CRM。
你可以通过下方的微信交流群,与 Cordys CRM 开源项目组进行交流和反馈。
<image height="150px" width="150px" alt="Cordys CRM QRCode" src="https://resource.fit2cloud.com/1panel/cordys-crm/img/wechat.png?v=20250904" />
## UI 展示
<table style="border-collapse: collapse; border: 1px solid black;">
<tr>
<td style="padding: 5px;background-color:#fff;"><img src= "https://resource.fit2cloud.com/1panel/cordys-crm/img/setting.png" alt="Settings" /></td>
<td style="padding: 5px;background-color:#fff;"><img src= "https://resource.fit2cloud.com/1panel/cordys-crm/img/rbac.png" alt="RBAC" /></td>
</tr>
<tr>
<td style="padding: 5px;background-color:#fff;"><img src= "https://resource.fit2cloud.com/1panel/cordys-crm/img/opportunity.png" alt="Opportunity List" /></td>
<td style="padding: 5px;background-color:#fff;"><img src= "https://resource.fit2cloud.com/1panel/cordys-crm/img/opportunity-detail.png" alt="Opportunity Detail" /></td>
</tr>
<tr>
<td style="padding: 5px;background-color:#fff;"><img src= "https://resource.fit2cloud.com/1panel/cordys-crm/img/bi.png" alt="BI" /></td>
<td style="padding: 5px;background-color:#fff;"><img src= "https://resource.fit2cloud.com/1panel/cordys-crm/img/ai.png" alt="AI" /></td>
</tr>
</table>
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=1Panel-dev/CordysCRM&type=date&legend=top-left)](https://www.star-history.com/#1Panel-dev/CordysCRM&type=date&legend=top-left)
## Roadmap
- [x] 2026.06: Cordys CRM 下载量突破 30 万次
- [x] 2026.05.29[v1.7.0 新增审批流、审批记录功能](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.7.0) 发布
- [x] 2026.03.26[v1.6.0 新增订单模块、计算组件函数功能增强](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.6.0) 发布
- [x] 2026.03.11[Cordys CRM Skills 正式发布](https://github.com/1Panel-dev/CordysCRM-skills)
- [x] 2026.01.29[v1.5.0 新增工商抬头管理、发票管理、回款管理](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.5.0)
- [x] 2025.12.18[v1.4.0 新增标讯、报价和合同模块](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.4.0)
- [x] 2025.12.12[v1.3.6](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.3.6) 发布
- [x] 2025.12.04[v1.3.5](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.3.5) 发布
- [x] 2025.11.28[v1.3.4](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.3.4) 发布
- [x] 2025.11.21[v1.3.3](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.3.3) 发布
- [x] 2025.11.14[v1.3.2](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.3.2) 发布
- [x] 2025.11.05[v1.3.1](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.3.1) 发布
- [x] 2025.11.03[v1.3.0](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.3.0) 发布,代码正式开源
- [x] 2025.10.17[v1.2.3](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.2.3) 发布
- [x] 2025.10.11[v1.2.2](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.2.2) 发布
- [x] 2025.09.26[v1.2.1](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.2.1) 发布
- [x] 2025.09.22Cordys CRM 下载量突破 10 万次
- [x] 2025.09.19[v1.2.0](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.2.0) 发布,开放 MCP Server并完成和 MaxKB 的对接
- [x] 2025.09.12[v1.1.9](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.1.9) 发布
- [x] 2025.09.05[v1.1.8](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.1.8) 发布
- [x] 2025.09.01[v1.1.7](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.1.7) 发布
- [x] 2025.08.27[v1.1.6](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.1.6) 发布
- [x] 2025.08.27[v1.1.5](https://github.com/1Panel-dev/CordysCRM/releases/tag/v1.1.5) 发布,开始公测
- [x] 2025.08:完成与 SQLBot 和 DataEase 的对接
- [x] 2025.07:吃自己的狗粮,成功替换飞致云使用 7 年的 Salesforce CRM
- [x] 2025.06v1.0 开发完成
- [x] 2024.09:写下第一行代码
## 技术栈
- 后端:[Spring Boot](https://spring.io/projects/spring-boot)
- 前端:[Vue.js](https://vuejs.org/) 、[Naive-UI](https://www.naiveui.com/) 、[Vant-UI](https://vant-ui.github.io/)
- 中间件:[MySQL](https://www.mysql.com/) , [Redis](https://redis.com/)
- 基础设施:[Docker](https://www.docker.com/)
## 飞致云旗下的其他明星项目
- [JumpServer](https://github.com/jumpserver/jumpserver/) - 广受欢迎的开源堡垒机
- [1Panel](https://github.com/1panel-dev/1panel/) - 现代化、开源的 Linux 服务器运维管理面板
- [MaxKB](https://github.com/1panel-dev/MaxKB/) - 强大易用的企业级智能体平台
- [DataEase](https://github.com/dataease/dataease/) - 人人可用的开源 BI 工具
- [SQLBot](https://github.com/dataease/SQLBot/) - 基于大模型和 RAG 的智能问数系统
- [MeterSphere](https://github.com/metersphere/metersphere/) - 新一代的开源持续测试工具
- [Halo](https://github.com/halo-dev/halo/) - 强大易用的开源建站工具
## License
本仓库遵循 [FIT2CLOUD Open Source License](LICENSE) 开源协议,该许可证本质上是 GPLv3但有一些额外的限制。
你可以基于 Cordys CRM 的源代码进行二次开发,但是需要遵守以下规定:
- 不能替换和修改 Cordys CRM 的 Logo 和版权信息;
- 二次开发后的衍生作品必须遵守 GPL V3 的开源义务。
如需商业授权,请联系:`support@fit2cloud.com`

17
SECURITY.md Normal file
View File

@@ -0,0 +1,17 @@
# 安全说明
如果您发现安全问题,请直接联系我们:
- support@fit2cloud.com
- 400-052-0755
感谢您的支持!
# Security Policy
All security bugs should be reported to the contact as below:
- support@fit2cloud.com
- 400-052-0755
Thanks for your support!

38
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
# Created by .ignore support plugin (hsz.mobi)
.DS_Store
node_modules
node/
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
*.iml
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
src/main/resources/static
src/main/resources/public
target
.settings
.project
.classpath
.factorypath
/crm/src/main/resources/packages/
/app/src/main/resources/static/
/crm/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker

123
backend/app/pom.xml Normal file
View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.cordys</groupId>
<artifactId>backend</artifactId>
<version>${revision}</version>
</parent>
<artifactId>app</artifactId>
<version>${revision}</version>
<name>app</name>
<dependencies>
<dependency>
<groupId>cn.cordys</groupId>
<artifactId>framework</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.cordys</groupId>
<artifactId>crm</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
<include>**/*.json</include>
<include>**/*.tpl</include>
<include>**/*.js</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
<plugins>
<!-- Spring Boot 插件,支持 Spring Boot 应用的构建 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
<loaderImplementation>CLASSIC</loaderImplementation>
</configuration>
</plugin>
<!-- Maven Clean 插件,清理资源文件夹中的静态文件 -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<configuration>
<filesets>
<fileset>
<directory>src/main/resources/static</directory>
<includes>
<include>**</include>
</includes>
<followSymlinks>false</followSymlinks>
</fileset>
</filesets>
</configuration>
</plugin>
<!-- Maven Antrun 插件,用于复制前端资源到静态目录 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>main-class-placement</id>
<phase>generate-resources</phase>
<configuration>
<skip>${skipAntRunForJenkins}</skip>
<target>
<!-- 复制移动端资源到静态目录 -->
<copy todir="src/main/resources/static/mobile" failonerror="false">
<fileset dir="../../frontend/packages/mobile/dist"/>
</copy>
<!-- 复制WEB端资源到静态目录 -->
<copy todir="src/main/resources/static" failonerror="false">
<fileset dir="../../frontend/packages/web/dist"/>
</copy>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Maven Surefire 插件,配置测试执行顺序 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
<argLine>${argLine} -Dfile.encoding=UTF-8</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,28 @@
package cn.cordys;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration;
import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration;
import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.PropertySource;
@SpringBootApplication(exclude = {
QuartzAutoConfiguration.class,
LdapAutoConfiguration.class,
Neo4jAutoConfiguration.class
})
@PropertySource(value = {
"classpath:commons.properties",
"file:/opt/cordys/conf/cordys-crm.properties",
}, encoding = "UTF-8", ignoreResourceNotFound = true)
@ServletComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,108 @@
package cn.cordys.listener;
import cn.cordys.common.service.DataInitService;
import cn.cordys.common.uid.impl.DefaultUidGenerator;
import cn.cordys.common.util.HikariCPUtils;
import cn.cordys.common.util.JSON;
import cn.cordys.common.util.rsa.RsaKey;
import cn.cordys.common.util.rsa.RsaUtils;
import cn.cordys.crm.system.service.ExportTaskStopService;
import cn.cordys.crm.system.service.ExtScheduleService;
import cn.cordys.crm.system.service.SystemService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@Component
@Slf4j
class AppListener implements ApplicationRunner {
@Resource
private DefaultUidGenerator uidGenerator;
@Resource
private ExtScheduleService extScheduleService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private DataInitService dataInitService;
@Resource
private ExportTaskStopService exportTaskStopService;
@Resource
private SystemService systemService;
/**
* 应用启动后执行的初始化方法。
* <p>
* 此方法会依次初始化唯一 ID 生成器、MinIO 配置和 RSA 配置。
* </p>
*
* @param args 启动参数
*/
@Override
public void run(ApplicationArguments args) {
log.info("===== 开始初始化配置 =====");
// 初始化唯一ID生成器
uidGenerator.init();
// 初始化RSA配置
log.info("初始化RSA配置");
initializeRsaConfiguration();
log.info("初始化定时任务");
extScheduleService.startEnableSchedules();
HikariCPUtils.printHikariCPStatus();
log.info("初始化默认组织数据");
dataInitService.initOneTime();
log.info("停止导出任务");
exportTaskStopService.stopPreparedAll();
log.info("清理表单缓存");
systemService.clearFormCache();
log.info("===== 完成初始化配置 =====");
}
/**
* 初始化 RSA 配置。
* <p>
* 此方法首先尝试加载现有的 RSA 密钥。如果不存在,则生成新的 RSA 密钥并保存到文件系统。
* </p>
*/
private void initializeRsaConfiguration() {
String redisKey = "rsa:key";
try {
// 从 Redis 获取 RSA 密钥
String rsaStr = stringRedisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotBlank(rsaStr)) {
// 如果 RSA 密钥存在,反序列化并设置密钥
RsaKey rsaKey = JSON.parseObject(rsaStr, RsaKey.class);
RsaUtils.setRsaKey(rsaKey);
return;
}
} catch (Exception e) {
log.error("从 Redis 获取 RSA 配置失败", e);
}
try {
// 如果 Redis 中没有密钥,生成新的 RSA 密钥并保存到 Redis
RsaKey rsaKey = RsaUtils.getRsaKey();
stringRedisTemplate.opsForValue().set(redisKey, JSON.toJSONString(rsaKey));
RsaUtils.setRsaKey(rsaKey);
} catch (Exception e) {
log.error("初始化 RSA 配置失败", e);
}
}
}

View File

@@ -0,0 +1,25 @@
package cn.cordys.listener;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* 自定义错误控制器类,用于处理应用中的错误页面请求。
* <p>
* 该控制器会将所有错误页面的请求重定向到网站的根页面("/")。
* </p>
*/
@Controller
public class CustomError implements ErrorController {
/**
* 错误处理方法,当发生错误时,会将请求重定向到根页面。
*
* @return 重定向到根页面
*/
@GetMapping("/error")
public String redirectRoot() {
return "redirect:/";
}
}

View File

@@ -0,0 +1,44 @@
package cn.cordys.listener;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* 主页控制器类,处理访问根页面("/")和登录页面("/login")的请求。
* <p>
* 该控制器负责将请求转发到 `index.html` 页面。
* </p>
*/
@Controller
public class Index {
/**
* 处理根路径("/")的请求,并返回首页 `index.html` 页面。
*
* @return 返回首页的视图名称
*/
@GetMapping("/web")
public String index() {
return "index.html";
}
/**
* 处理移动端根路径("/")的请求,并返回首页 `/mobile/index.html` 页面。
*
* @return 返回首页的视图名称
*/
@GetMapping("/mobile")
public String mobileIndex() {
return "mobile/index.html";
}
/**
* 处理登录页面("/login")的请求,并返回 `index.html` 页面。
*
* @return 返回登录页面的视图名称
*/
@GetMapping(value = "/login")
public String login() {
return "/index.html";
}
}

View File

@@ -0,0 +1,7 @@
██████╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗███████╗ ██████╗██████╗ ███╗ ███╗
██╔════╝██╔═══██╗██╔══██╗██╔══██╗╚██╗ ██╔╝██╔════╝ ██╔════╝██╔══██╗████╗ ████║
██║ ██║ ██║██████╔╝██║ ██║ ╚████╔╝ ███████╗ ██║ ██████╔╝██╔████╔██║
██║ ██║ ██║██╔══██╗██║ ██║ ╚██╔╝ ╚════██║ ██║ ██╔══██╗██║╚██╔╝██║
╚██████╗╚██████╔╝██║ ██║██████╔╝ ██║ ███████║ ╚██████╗██║ ██║██║ ╚═╝ ██║
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝

View File

@@ -0,0 +1,93 @@
# Application Settings
spring.application.name=cordys-crm
server.port=8081
# Compression Settings (gzip)
server.compression.enabled=true
server.compression.mime-types=application/json,application/xml,text/html,text/xml,text/plain,application/javascript,text/css,text/javascript,image/jpeg
server.compression.min-response-size=2048
# Logging Settings
logging.file.path=/opt/cordys/logs/cordys-crm
# DataSource Configuration (HikariCP)
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.maximum-pool-size=100
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.pool-name=DatebookHikariCP
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1
# Quartz Scheduler DataSource Settings
quartz.enabled=true
quartz.scheduler-name=cordys-crm-quartz
quartz.thread-count=10
quartz.properties.org.quartz.jobStore.acquireTriggersWithinLock=true
spring.datasource.quartz.url=${spring.datasource.url}
spring.datasource.quartz.username=${spring.datasource.username}
spring.datasource.quartz.password=${spring.datasource.password}
spring.datasource.quartz.hikari.maximum-pool-size=50
spring.datasource.quartz.hikari.minimum-idle=10
spring.datasource.quartz.hikari.idle-timeout=300000
spring.datasource.quartz.hikari.auto-commit=true
spring.datasource.quartz.hikari.pool-name=DatebookHikariCP
spring.datasource.quartz.hikari.max-lifetime=1800000
spring.datasource.quartz.hikari.connection-timeout=30000
spring.datasource.quartz.hikari.connection-test-query=SELECT 1
# MyBatis Configuration
mybatis.configuration.cache-enabled=false
mybatis.configuration.lazy-loading-enabled=false
mybatis.configuration.aggressive-lazy-loading=true
mybatis.configuration.use-column-label=true
mybatis.configuration.auto-mapping-behavior=full
mybatis.configuration.default-statement-timeout=25000
mybatis.configuration.map-underscore-to-camel-case=true
# Virtual Thread Settings (for Thread Management)
spring.threads.virtual.enabled=true
spring.mvc.log-request-details=false
# Flyway Database Migration Configuration
spring.flyway.enabled=true
spring.flyway.baseline-on-migrate=true
spring.flyway.locations=classpath:migration
spring.flyway.table=cordys_crm_version
spring.flyway.baseline-version=0
spring.flyway.encoding=UTF-8
spring.flyway.validate-on-migrate=false
# File Upload Configuration
spring.servlet.multipart.max-file-size=1024MB
spring.servlet.multipart.max-request-size=1024MB
# Redisson (Session Management with Redis)
spring.session.timeout=43200s
spring.session.redis.repository-type=indexed
spring.cache.type=redis
#spring.redis.redisson.file=file:/opt/cordys/conf/redisson.yml
# Template Engines (Freemarker, Groovy)
spring.freemarker.check-template-location=false
spring.groovy.template.check-template-location=false
# Swagger Configuration (API Documentation)
springdoc.swagger-ui.enabled=true
springdoc.api-docs.enabled=true
springdoc.api-docs.groups.enabled=true
# i18n
spring.messages.basename=i18n/cordys-crm
# Enable whitelist functionality, if not enabled, access will not be restricted.
allowed.ip.ranges.enabled=false
# Enable whitelist functionality, if not enabled, access will not be restricted.
allowed.ip.ranges=
# List of URLs that require XSS filtering, supports Ant-style path matching, e.g., /api/**. If no URLs need to be filtered, it can be left empty.
# xss.protection.url.list=/account/follow/**,/announcement/add

View File

@@ -0,0 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<property resource="commons.properties"/>
<property file="/opt/cordys/conf/cordys-crm.properties" ignoreResourceNotFound="true"/>
<!-- Console 输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %5p %40.40c:%4L - %m%n</pattern>
</encoder>
</appender>
<!-- ===================== 文件 Appender ===================== -->
<appender name="traceAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>TRACE</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<File>${logging.file.path}/trace.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${logging.file.path}/history/trace.%d{yyyyMMdd}-%i.log</FileNamePattern>
<maxHistory>${logger.max.history:-30}</maxHistory>
<maxFileSize>50MB</maxFileSize>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<Pattern>%d [%thread] %-5level %logger{36} %line - %msg%n</Pattern>
</encoder>
</appender>
<appender name="debugAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<File>${logging.file.path}/debug.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${logging.file.path}/history/debug.%d{yyyyMMdd}-%i.log</FileNamePattern>
<maxHistory>${logger.max.history:-30}</maxHistory>
<maxFileSize>50MB</maxFileSize>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<Pattern>%d [%thread] %-5level %logger{36} %line - %msg%n</Pattern>
</encoder>
</appender>
<appender name="infoAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<File>${logging.file.path}/info.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${logging.file.path}/history/info.%d{yyyyMMdd}-%i.log</FileNamePattern>
<maxHistory>${logger.max.history:-30}</maxHistory>
<maxFileSize>50MB</maxFileSize>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<Pattern>%d [%thread] %-5level %logger{36} %line - %msg%n</Pattern>
</encoder>
</appender>
<appender name="warnAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<File>${logging.file.path}/warn.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${logging.file.path}/history/warn.%d{yyyyMMdd}-%i.log</FileNamePattern>
<maxHistory>${logger.max.history:-30}</maxHistory>
<maxFileSize>50MB</maxFileSize>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<Pattern>%d [%thread] %-5level %logger{36} %line - %msg%n</Pattern>
</encoder>
</appender>
<appender name="errorAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<File>${logging.file.path}/error.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${logging.file.path}/history/error.%d{yyyyMMdd}-%i.log</FileNamePattern>
<maxHistory>${logger.max.history:-30}</maxHistory>
<maxFileSize>50MB</maxFileSize>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<Pattern>%d [%thread] %-5level %logger{36} %line - %msg%n</Pattern>
</encoder>
</appender>
<!-- ===================== Async Appender ===================== -->
<appender name="traceAsyncAppender" class="ch.qos.logback.classic.AsyncAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>TRACE</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<queueSize>10000</queueSize>
<appender-ref ref="traceAppender"/>
</appender>
<appender name="debugAsyncAppender" class="ch.qos.logback.classic.AsyncAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<queueSize>10000</queueSize>
<appender-ref ref="debugAppender"/>
</appender>
<appender name="infoAsyncAppender" class="ch.qos.logback.classic.AsyncAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<queueSize>10000</queueSize>
<appender-ref ref="infoAppender"/>
</appender>
<appender name="warnAsyncAppender" class="ch.qos.logback.classic.AsyncAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<queueSize>10000</queueSize>
<includeCallerData>true</includeCallerData>
<appender-ref ref="warnAppender"/>
</appender>
<appender name="errorAsyncAppender" class="ch.qos.logback.classic.AsyncAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<queueSize>10000</queueSize>
<includeCallerData>true</includeCallerData>
<appender-ref ref="errorAppender"/>
</appender>
<!-- ===================== Logger ===================== -->
<!-- cn.cordys 模块日志 -->
<logger name="cn.cordys" additivity="false" level="${logback.level:INFO}">
<appender-ref ref="traceAsyncAppender"/>
<appender-ref ref="debugAsyncAppender"/>
<appender-ref ref="infoAsyncAppender"/>
<appender-ref ref="warnAsyncAppender"/>
<appender-ref ref="errorAsyncAppender"/>
<appender-ref ref="console"/>
</logger>
<!-- cn.cordys.Application 单独 INFO 输出 -->
<logger name="cn.cordys.Application" additivity="false" level="${logback.level:INFO}">
<appender-ref ref="infoAsyncAppender"/>
</logger>
<!-- 容器日志 -->
<logger name="org.eclipse.jetty.ee10.servlet.ServletChannel" level="ERROR"/>
<!-- ===================== Root ===================== -->
<root level="INFO">
<appender-ref ref="console"/>
</root>
</configuration>

34
backend/crm/pom.xml Normal file
View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.cordys</groupId>
<artifactId>backend</artifactId>
<version>${revision}</version>
</parent>
<artifactId>crm</artifactId>
<version>${revision}</version>
<dependencies>
<dependency>
<groupId>cn.cordys</groupId>
<artifactId>framework</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,401 @@
package cn.cordys.common.constants;
import cn.cordys.crm.system.dto.field.base.BaseField;
import cn.cordys.crm.system.dto.field.base.SubField;
import lombok.Getter;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.Strings;
import java.util.*;
import java.util.stream.Collectors;
/**
* 业务模块字段(定义在主表中,有特定业务含义)(标准字段)
*
* @Author: jianxing
* @CreateTime: 2025-02-18 17:27
*/
@Getter
public enum BusinessModuleField {
/*------ start: CUSTOMER ------*/
/**
* 客户名称
*/
CUSTOMER_NAME("customerName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.CUSTOMER.getKey()),
/**
* 负责人
*/
CUSTOMER_OWNER("customerOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.CUSTOMER.getKey()),
/*------ end: CUSTOMER ------*/
/*------ start: CLUE ------*/
/**
* 线索名称
*/
CLUE_NAME("clueName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.CLUE.getKey()),
/**
* 负责人
*/
CLUE_OWNER("clueOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.CLUE.getKey()),
/**
* 联系人
*/
CLUE_CONTACT("clueContactName", "contact", Set.of(), FormKey.CLUE.getKey()),
/**
* 联系人电话
*/
CLUE_CONTACT_PHONE("clueContactPhone", "phone", Set.of(), FormKey.CLUE.getKey()),
/**
* 意向产品
*/
CLUE_PRODUCTS("clueProduct", "products", Set.of(), FormKey.CLUE.getKey()),
/*------ end: CUSTOMER ------*/
/*------ start: CUSTOMER_MANAGEMENT_CONTACT ------*/
/**
* 联系人客户id
*/
CUSTOMER_CONTACT_CUSTOMER("contactCustomer", "customerId", Set.of(), FormKey.CONTACT.getKey()),
/**
* 联系人责任人
*/
CUSTOMER_CONTACT_OWNER("contactOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.CONTACT.getKey()),
/**
* 联系人名称
*/
CUSTOMER_CONTACT_NAME("contactName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.CONTACT.getKey()),
/**
* 联系人电话
*/
CUSTOMER_CONTACT_PHONE("contactPhone", "phone", Set.of(), FormKey.CONTACT.getKey()),
/*------ end: CUSTOMER_MANAGEMENT_CONTACT ------*/
/*------ start: OPPORTUNITY ------*/
/**
* 商机名称
*/
OPPORTUNITY_NAME("opportunityName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.OPPORTUNITY.getKey()),
/**
* 客户名称
*/
OPPORTUNITY_CUSTOMER_NAME("opportunityCustomer", "customerId", Set.of(), FormKey.OPPORTUNITY.getKey()),
/**
* 商机金额
*/
OPPORTUNITY_AMOUNT("opportunityPrice", "amount", Set.of(), FormKey.OPPORTUNITY.getKey()),
/**
* 可能性
*/
OPPORTUNITY_POSSIBLE("opportunityWinRate", "possible", Set.of(), FormKey.OPPORTUNITY.getKey()),
/**
* 结束时间
*/
OPPORTUNITY_END_TIME("opportunityEndTime", "expectedEndTime", Set.of(), FormKey.OPPORTUNITY.getKey()),
/**
* 意向产品
*/
OPPORTUNITY_PRODUCTS("opportunityProduct", "products", Set.of(), FormKey.OPPORTUNITY.getKey()),
/**
* 联系人
*/
OPPORTUNITY_CONTACT("opportunityContact", "contactId", Set.of(), FormKey.OPPORTUNITY.getKey()),
/**
* 负责人
*/
OPPORTUNITY_OWNER("opportunityOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.OPPORTUNITY.getKey()),
/*------ end: OPPORTUNITY ------*/
/*------ start: FOLLOW_UP_RECORD ------*/
/**
* 跟进类型
*/
FOLLOW_RECORD_TYPE("recordType", "type", Set.of("options", "rules.required", "mobile", "readable"), FormKey.FOLLOW_RECORD.getKey()),
/**
* 客户id
*/
FOLLOW_RECORD_CUSTOMER("recordCustomer", "customerId", Set.of("rules.required", "mobile", "readable"), FormKey.FOLLOW_RECORD.getKey()),
/**
* 商机id
*/
FOLLOW_RECORD_OPPORTUNITY("recordOpportunity", "opportunityId", Set.of(), FormKey.FOLLOW_RECORD.getKey()),
/**
* 线索id
*/
FOLLOW_RECORD_CLUE("recordClue", "clueId", Set.of("rules.required", "mobile", "readable"), FormKey.FOLLOW_RECORD.getKey()),
/**
* 责任人id
*/
FOLLOW_RECORD_OWNER("recordOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.FOLLOW_RECORD.getKey()),
/**
* 联系人id
*/
FOLLOW_RECORD_CONTACT("recordContact", "contactId", Set.of(), FormKey.FOLLOW_RECORD.getKey()),
/**
* 跟进内容
*/
FOLLOW_RECORD_CONTENT("recordDescription", "content", Set.of(), FormKey.FOLLOW_RECORD.getKey()),
/**
* 跟进时间
*/
FOLLOW_RECORD_TIME("recordTime", "followTime", Set.of(), FormKey.FOLLOW_RECORD.getKey()),
/**
* 跟进方式
*/
FOLLOW_METHOD("recordMethod", "followMethod", Set.of(), FormKey.FOLLOW_RECORD.getKey()),
/*------ end: FOLLOW_UP_RECORD ------*/
/*------ start: FOLLOW_UP_PLAN ------*/
/**
* 跟进类型
*/
FOLLOW_PLAN_TYPE("planType", "type", Set.of("options", "rules.required", "mobile", "readable"), FormKey.FOLLOW_PLAN.getKey()),
/**
* 客户id
*/
FOLLOW_PLAN_CUSTOMER("planCustomer", "customerId", Set.of("rules.required", "mobile", "readable"), FormKey.FOLLOW_PLAN.getKey()),
/**
* 商机id
*/
FOLLOW_PLAN_OPPORTUNITY("planOpportunity", "opportunityId", Set.of(), FormKey.FOLLOW_PLAN.getKey()),
/**
* 线索id
*/
FOLLOW_PLAN_CLUE("planClue", "clueId", Set.of("rules.required", "mobile", "readable"), FormKey.FOLLOW_PLAN.getKey()),
/**
* 责任人id
*/
FOLLOW_PLAN_OWNER("planOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.FOLLOW_PLAN.getKey()),
/**
* 联系人id
*/
FOLLOW_PLAN_CONTACT("planContact", "contactId", Set.of(), FormKey.FOLLOW_PLAN.getKey()),
/**
* 预计开始时间
*/
FOLLOW_PLAN_ESTIMATED_TIME("planStartTime", "estimatedTime", Set.of(), FormKey.FOLLOW_PLAN.getKey()),
/**
* 预计沟通内容
*/
FOLLOW_PLAN_CONTENT("planContent", "content", Set.of(), FormKey.FOLLOW_PLAN.getKey()),
/**
* 跟进方式
*/
FOLLOW_PLAN_METHOD("planMethod", "method", Set.of(), FormKey.FOLLOW_PLAN.getKey()),
/*------ end: FOLLOW_UP_PLAN ------*/
/*------ start: PRODUCT ------*/
PRODUCT_NAME("productName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.PRODUCT.getKey()),
PRODUCT_PRICE("productPrice", "price", Set.of(), FormKey.PRODUCT.getKey()),
PRODUCT_STATUS("productStatus", "status", Set.of("rules.required", "mobile", "readable"), FormKey.PRODUCT.getKey()),
/*------ end: PRODUCT ------*/
/**
* 价格表单 (修改价格子表格为自定义时, 注意处理对应详情解析逻辑)
*/
PRICE_NAME("priceName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.PRICE.getKey()),
PRICE_STATUS("priceStatus", "status", Set.of("rules.required", "mobile", "readable"), FormKey.PRICE.getKey()),
PRICE_PRODUCT_TABLE("priceProducts", "products", Set.of("mobile", "readable"), FormKey.PRICE.getKey()),
PRICE_PRODUCT("priceProduct", "product", Set.of("rules.required", "mobile", "dataSourceType", "readable"), FormKey.PRICE.getKey()),
PRICE_PRODUCT_AMOUNT("priceProductAmount", "amount", Set.of("rules.required", "mobile", "readable"), FormKey.PRICE.getKey()),
/**
* 报价单表单
*/
QUOTATION_NAME("quotationName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.QUOTATION.getKey()),
QUOTATION_OPPORTUNITY("quotationOpportunity", "opportunityId", Set.of("rules.required", "mobile", "dataSourceType", "readable"), FormKey.QUOTATION.getKey()),
QUOTATION_UNTIL_TIME("quotationUntilTime", "untilTime", Set.of("rules.required", "mobile", "readable"), FormKey.QUOTATION.getKey()),
QUOTATION_TOTAL_AMOUNT("quotationTotalAmount", "amount", Set.of("rules.required", "mobile", "readable"), FormKey.QUOTATION.getKey()),
/*------ start: CONTRACT_PAYMENT_PLAN ------*/
/**
* 负责人
*/
CONTRACT_PAYMENT_PLAN_OWNER("contractPaymentPlanOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_PLAN.getKey()),
/**
* 合同
*/
CONTRACT_PAYMENT_PLAN_CONTRACT("contractPaymentPlanContract", "contractId", Set.of("rules.required", "dataSourceType", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_PLAN.getKey()),
/**
* 计划回款金额
*/
CONTRACT_PAYMENT_PLAN_PLAN_AMOUNT("contractPaymentPlanPlanAmount", "planAmount", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_PLAN.getKey()),
/**
* 计划回款时间
*/
CONTRACT_PAYMENT_PLAN_PLAN_END_TIME("contractPaymentPlanPlanEndTime", "planEndTime", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_PLAN.getKey()),
/**
* 回款计划名称
*/
CONTRACT_PAYMENT_PLAN_NAME("contractPaymentPlanName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_PLAN.getKey()),
/*------ end: CONTRACT_PAYMENT_PLAN ------*/
/*------ start: CONTRACT ------*/
/**
* 合同名稱
*/
CONTRACT_NAME("contractName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT.getKey()),
CONTRACT_CUSTOMER_NAME("contractCustomer", "customerId", Set.of("rules.required", "mobile", "readable", "dataSourceType"), FormKey.CONTRACT.getKey()),
CONTRACT_OWNER("contractOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT.getKey()),
CONTRACT_NO("contractNo", "number", Set.of("rules.required"), FormKey.CONTRACT.getKey()),
CONTRACT_START_TIME("contractStartTime", "startTime", Set.of("mobile", "readable"), FormKey.CONTRACT.getKey()),
CONTRACT_END_TIME("contractEndTime", "endTime", Set.of("mobile", "readable"), FormKey.CONTRACT.getKey()),
CONTRACT_TOTAL_AMOUNT("contractTotalAmount", "amount", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT.getKey()),
/*------ end: CONTRACT ------*/
/**
* 发票
*/
/*------ start: CONTRACT_INVOICE ------*/
INVOICE_NAME("invoiceName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.INVOICE.getKey()),
INVOICE_OWNER("invoiceOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.INVOICE.getKey()),
INVOICE_AMOUNT("invoiceAmount", "amount", Set.of("rules.required", "readable", "mobile"), FormKey.INVOICE.getKey()),
INVOICE_CONTRACT_ID("invoiceContract", "contractId", Set.of("rules.required", "mobile", "readable", "dataSourceType"), FormKey.INVOICE.getKey()),
INVOICE_INVOICE_TYPE("invoiceType", "invoiceType", Set.of("rules.required", "readable", "mobile"), FormKey.INVOICE.getKey()),
INVOICE_TAX_RATE("invoiceTaxRate", "taxRate", Set.of("rules.required", "readable", "mobile"), FormKey.INVOICE.getKey()),
INVOICE_BUSINESS_TITLE_ID("invoiceBusinessTitle", "businessTitleId", Set.of("rules.required", "mobile", "readable", "dataSourceType"), FormKey.INVOICE.getKey()),
/*------ end: CONTRACT_INVOICE ------*/
/*------ start: CONTRACT_PAYMENT_RECORD 合同回款记录 ------*/
CONTRACT_PAYMENT_RECORD_NO("contractPaymentRecordNo", "no", Set.of("rules.required"), FormKey.CONTRACT_PAYMENT_RECORD.getKey()),
CONTRACT_PAYMENT_RECORD_NAME("contractPaymentRecordName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_RECORD.getKey()),
CONTRACT_PAYMENT_RECORD_OWNER("contractPaymentRecordOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_RECORD.getKey()),
CONTRACT_PAYMENT_RECORD_CONTRACT("contractPaymentRecordContract", "contractId", Set.of("rules.required", "dataSourceType", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_RECORD.getKey()),
CONTRACT_PAYMENT_RECORD_PLAN("contractPaymentRecordPlan", "paymentPlanId", Set.of("dataSourceType", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_RECORD.getKey()),
CONTRACT_PAYMENT_RECORD_AMOUNT("contractPaymentRecordAmount", "recordAmount", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_RECORD.getKey()),
CONTRACT_PAYMENT_RECORD_END_TIME("contractPaymentRecordEndTime", "recordEndTime", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_RECORD.getKey()),
/*------ end: CONTRACT_PAYMENT_RECORD 合同回款记录 ------*/
/*------ start: ORDER ------*/
ORDER_NAME("orderName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.ORDER.getKey()),
ORDER_CUSTOMER("orderCustomer", "customerId", Set.of("dataSourceType"), FormKey.ORDER.getKey()),
ORDER_CONTRACT("orderContract", "contractId", Set.of("dataSourceType"), FormKey.ORDER.getKey()),
ORDER_OWNER("orderOwner", "owner", Set.of(), FormKey.ORDER.getKey()),
ORDER_NO("orderNo", "number", Set.of("rules.required"), FormKey.ORDER.getKey()),
ORDER_TOTAL_AMOUNT("orderAmount", "amount", Set.of("rules.required", "mobile", "readable"), FormKey.ORDER.getKey()),
/*------ end: ORDER ------*/
/*------ start: ORDER ------*/
CUSTOM_FORM_DATA_NAME("customFormDataName", "name", Set.of("rules.required", "mobile", "readable"), null),
CUSTOM_FORM_DATA_OWNER("customFormDataNOwner", "owner", Set.of("rules.required", "mobile", "readable"), null),
/*------ end: ORDER ------*/
;
/**
* 业务字段缓存
*/
private static final Map<String, BusinessModuleField> INTERNAL_CACHE = new HashMap<>();
static {
for (BusinessModuleField field : values()) {
// 防止ofKey方法频繁调用
INTERNAL_CACHE.put(field.key, field);
}
}
/**
* 字段 keyfield.json 中的 internalKey
*/
private final String key;
/**
* 业务字段 key
*/
private final String businessKey;
/**
* 禁止修改的参数列表
*/
private final Set<String> disabledProps;
/**
* 表单 key
*/
private final String formKey;
BusinessModuleField(String key, String businessKey, Set<String> disabledProps, String formKey) {
this.key = key;
this.businessKey = businessKey;
this.disabledProps = disabledProps;
this.formKey = formKey;
}
/**
* 判断业务字段是否被删除
*
* @param formKey 表单 key
* @param fields 字段集合
* @return 是否被删除
*/
public static boolean isBusinessDeleted(String formKey, List<BaseField> fields) {
List<BusinessModuleField> formBusinessFields;
if (FormKey.ofKey(formKey) == null) {
// 如果不是内置表单,则校验自定义表单字段必须要名字和负责人
formBusinessFields = List.of(BusinessModuleField.CUSTOM_FORM_DATA_NAME, BusinessModuleField.CUSTOM_FORM_DATA_OWNER);
} else {
formBusinessFields = Arrays.stream(BusinessModuleField.values()).filter(field -> Strings.CS.equals(formKey, field.getFormKey())).toList();
}
if (CollectionUtils.isEmpty(formBusinessFields)) {
return false;
}
return formBusinessFields.stream()
.anyMatch(businessField ->
fields.stream().noneMatch(field -> Strings.CS.equals(businessField.getKey(), field.getInternalKey()))
&& businessField.noneMatchOfSubFields(fields)
);
}
/**
* 判断子表字段中是否存在业务字段
*
* @param fields 字段集合
* @return 是否存在业务字段
*/
private boolean noneMatchOfSubFields(List<BaseField> fields) {
boolean noneMatch = true;
for (BaseField field : fields) {
if (field instanceof SubField subField && CollectionUtils.isNotEmpty(subField.getSubFields())) {
noneMatch = subField.getSubFields().stream().noneMatch(sub -> Strings.CS.equals(this.getKey(), sub.getInternalKey()));
if (!noneMatch) {
break;
}
}
}
return noneMatch;
}
/**
* 判断是否有重复的字段
*
* @param fields 字段集合
* @return 是否有重复的字段
*/
public static boolean hasRepeatName(List<BaseField> fields) {
return fields.stream()
.collect(Collectors.groupingBy(BaseField::getName, Collectors.counting()))
.values().stream()
.anyMatch(count -> count > 1);
}
/**
* 通过Key查询业务字段
*
* @param internalKey 业务key
* @return 业务字段
*/
public static BusinessModuleField ofKey(String internalKey) {
return INTERNAL_CACHE.get(internalKey);
}
}

View File

@@ -0,0 +1,21 @@
package cn.cordys.common.constants;
/**
* 客户,线索,商机等业务数据的搜索类型
*
* @author jianxing
*/
public enum BusinessSearchType {
/**
* 全部数据
*/
ALL,
/**
* 负责人是我的数据
*/
SELF,
/**
* 有数据权限的部门的数据
*/
DEPARTMENT
}

View File

@@ -0,0 +1,13 @@
package cn.cordys.common.constants;
/**
* @Author: jianxing
* @CreateTime: 2025-10-14 10:11
*/
public enum ChartAggregateMethod {
SUM,
AVG,
COUNT,
MAX,
MIN
}

View File

@@ -0,0 +1,35 @@
package cn.cordys.common.constants;
import cn.cordys.common.exception.IResultCode;
/**
* 通用功能状态码
* 通用功能返回的状态码
*
* @author jianxing
*/
public enum CommonResultCode implements IResultCode {
FIELD_VALIDATE_ERROR(100001, "field_validate_error"),
FIELD_OPTION_VALUE_ERROR(100002, "field_option_value_error"),
APPROVAL_NOT_ENABLED_ERROR(100003, "approval.not.enabled");
private final int code;
private final String message;
CommonResultCode(int code, String message) {
this.code = code;
this.message = message;
}
@Override
public int getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}

View File

@@ -0,0 +1,96 @@
package cn.cordys.common.constants;
import lombok.Getter;
import org.apache.commons.lang3.Strings;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author song-cc-rock
*/
@Getter
public enum FormKey {
/**
* 线索
*/
CLUE("clue"),
/**
* 客户
*/
CUSTOMER("customer"),
/**
* 联系人
*/
CONTACT("contact"),
/**
* 跟进记录
*/
FOLLOW_RECORD("record"),
/**
* 跟进计划
*/
FOLLOW_PLAN("plan"),
/**
* 商机
*/
OPPORTUNITY("opportunity"),
/**
* 产品
*/
PRODUCT("product"),
/**
* 价格
*/
PRICE("price"),
/**
* 报价单
*/
QUOTATION("quotation"),
/**
* 合同
*/
CONTRACT("contract"),
/**
* 发票
*/
INVOICE("invoice"),
/**
* 合同回款计划
*/
CONTRACT_PAYMENT_PLAN("contractPaymentPlan"),
/**
* 回款记录
*/
CONTRACT_PAYMENT_RECORD("contractPaymentRecord"),
/**
* 订单
*/
ORDER("order");
private final String key;
FormKey(String key) {
this.key = key;
}
public static List<String> allKeys() {
return Arrays.stream(FormKey.values()).map(FormKey::getKey).collect(Collectors.toList());
}
public static FormKey ofKey(String key) {
for (FormKey formKey : FormKey.values()) {
if (Strings.CI.equals(formKey.getKey(), key)) {
return formKey;
}
}
return null;
}
public boolean hasSnapshot() {
return Strings.CI.equalsAny(this.key, CONTRACT.getKey(), INVOICE.getKey(), QUOTATION.getKey(), ORDER.getKey());
}
}

View File

@@ -0,0 +1,25 @@
package cn.cordys.common.constants;
import lombok.Getter;
/**
* @author song-cc-rock
*/
@Getter
public class FormKeyConstants {
public static final String ORDER = "order";
public static final String CLUE = "clue";
public static final String CUSTOMER = "customer";
public static final String CONTRACT = "contract";
public static final String CONTRACT_INVOICE = "contractInvoice";
public static final String CONTRACT_PAYMENT_PLAN = "contractPaymentPlan";
public static final String CONTRACT_PAYMENT_RECORD = "contractPaymentRecord";
public static final String OPPORTUNITY = "opportunity";
public static final String QUOTATION = "quotation";
public static final String FOLLOW_PLAN = "plan";
public static final String FOLLOW_RECORD = "record";
}

View File

@@ -0,0 +1,13 @@
package cn.cordys.common.constants;
public enum HttpMethodConstants {
GET,
HEAD,
POST,
PUT,
PATCH,
DELETE,
OPTIONS,
TRACE,
CONNECT
}

View File

@@ -0,0 +1,22 @@
package cn.cordys.common.constants;
import lombok.Getter;
/**
* 系统内置角色ID
*
* @author jianxing
*/
@Getter
public enum InternalRole {
ORG_ADMIN("org_admin"),
SALES_MANAGER("sales_manager"),
SALES_STAFF("sales_staff");
private final String value;
InternalRole(String value) {
this.value = value;
}
}

View File

@@ -0,0 +1,71 @@
package cn.cordys.common.constants;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author: jianxing
* @CreateTime: 2025-07-25 14:05
*/
public enum InternalUserView {
/**
* 全部视图
*/
ALL,
/**
* 个人视图
*/
SELF,
/**
* 部门视图
*/
DEPARTMENT,
/**
* 协作客户视图
*/
CUSTOMER_COLLABORATION,
/**
* 赢单视图
*/
OPPORTUNITY_SUCCESS;
public static final String CURRENT_USER = "CURRENT_USER";
public static List<String> getCurrentUserArrayValue() {
List<String> values = new ArrayList<>(0);
values.add(CURRENT_USER);
return values;
}
public static boolean isInternalUserView(String viewId) {
if (StringUtils.isBlank(viewId)) {
return false;
}
return Arrays.stream(InternalUserView.values())
.map(InternalUserView::name)
.collect(Collectors.toSet()).contains(viewId);
}
public static boolean isAll(String searchType) {
return Strings.CS.equals(ALL.name(), searchType);
}
public static boolean isSelf(String searchType) {
return Strings.CS.equals(SELF.name(), searchType);
}
public static boolean isDepartment(String searchType) {
return Strings.CS.equals(DEPARTMENT.name(), searchType);
}
public static boolean isVisible(String searchType) {
return Strings.CS.equals(CUSTOMER_COLLABORATION.name(), searchType);
}
}

View File

@@ -0,0 +1,50 @@
package cn.cordys.common.constants;
/**
* 联动场景
*
* @author song-cc-rock
*/
public enum LinkScenarioKey {
/**
* 线索转客户
*/
CLUE_TO_CUSTOMER,
/**
* 线索转联系人
*/
CLUE_TO_CONTACT,
/**
* 线索转商机
*/
CLUE_TO_OPPORTUNITY,
/**
* 客户转商机
*/
CUSTOMER_TO_OPPORTUNITY,
/**
* 线索转记录
*/
CLUE_TO_RECORD,
/**
* 客户转记录
*/
CUSTOMER_TO_RECORD,
/**
* 商机转记录
*/
OPPORTUNITY_TO_RECORD,
/**
* 计划转记录
*/
PLAN_TO_RECORD,
/**
* 合同转发票
*/
CONTRACT_TO_INVOICE,
/**
* 合同转订单
*/
CONTRACT_TO_ORDER,
}

View File

@@ -0,0 +1,46 @@
package cn.cordys.common.constants;
import lombok.Getter;
@Getter
public enum ModuleKey {
/**
* 首页
*/
HOME("home"),
/**
* 线索管理
*/
CLUE("clue"),
/**
* 客户管理
*/
CUSTOMER("customer"),
/**
* 商机管理
*/
BUSINESS("business"),
/**
* 产品管理
*/
PRODUCT("product"),
/**
* 系统设置
*/
SETTING("setting");
/**
* *******************************************
* 注意:
* 新增菜单不要在moduleKey中添加了
* *******************************************
*/
private final String key;
ModuleKey(String key) {
this.key = key;
}
}

View File

@@ -0,0 +1,258 @@
package cn.cordys.common.constants;
/**
* @author jianxing
* @date 2025-01-03 11:31:40
*/
public class PermissionConstants {
/*------ start: SYSTEM_ROLE ------*/
public static final String SYSTEM_ROLE_READ = "SYSTEM_ROLE:READ";
public static final String SYSTEM_ROLE_ADD = "SYSTEM_ROLE:ADD";
public static final String SYSTEM_ROLE_UPDATE = "SYSTEM_ROLE:UPDATE";
public static final String SYSTEM_ROLE_DELETE = "SYSTEM_ROLE:DELETE";
public static final String SYSTEM_ROLE_ADD_USER = "SYSTEM_ROLE:ADD_USER";
public static final String SYSTEM_ROLE_REMOVE_USER = "SYSTEM_ROLE:REMOVE_USER";
/*------ end: SYSTEM_ROLE------*/
/*------ start: OPERATION_LOG ------*/
public static final String OPERATION_LOG_READ = "OPERATION_LOG:READ";
/*------ end: OPERATION_LOG ------*/
/*------ start: SYSTEM_NOTICE ------*/
public static final String SYSTEM_NOTICE_READ = "SYSTEM_NOTICE:READ";
public static final String SYSTEM_NOTICE_ADD = "SYSTEM_NOTICE:ADD";
public static final String SYSTEM_NOTICE_UPDATE = "SYSTEM_NOTICE:UPDATE";
public static final String SYSTEM_NOTICE_DELETE = "SYSTEM_NOTICE:DELETE";
/*------ end: SYSTEM_NOTICE ------*/
/*------ start: SYS_DEPARTMENT ------*/
public static final String SYS_ORGANIZATION_READ = "SYS_ORGANIZATION:READ";
public static final String SYS_ORGANIZATION_ADD = "SYS_ORGANIZATION:ADD";
public static final String SYS_ORGANIZATION_UPDATE = "SYS_ORGANIZATION:UPDATE";
public static final String SYS_ORGANIZATION_DELETE = "SYS_ORGANIZATION:DELETE";
public static final String SYS_ORGANIZATION_IMPORT = "SYS_ORGANIZATION:IMPORT";
public static final String SYS_ORGANIZATION_SYNC = "SYS_ORGANIZATION:SYNC";
public static final String SYS_ORGANIZATION_USER_RESET_PASSWORD = "SYS_ORGANIZATION_USER:RESET_PASSWORD";
/*------ end: SYS_DEPARTMENT ------*/
/*------ start: SYSTEM_SETTING ------*/
public static final String SYSTEM_SETTING_READ = "SYSTEM_SETTING:READ";
public static final String SYSTEM_SETTING_UPDATE = "SYSTEM_SETTING:UPDATE";
public static final String SYSTEM_SETTING_ADD = "SYSTEM_SETTING:ADD";
public static final String SYSTEM_SETTING_DELETE = "SYSTEM_SETTING:DELETE";
/*------ end: SYSTEM_SETTING ------*/
/**
* module setting permission
*/
public static final String MODULE_SETTING_READ = "MODULE_SETTING:READ";
public static final String MODULE_SETTING_UPDATE = "MODULE_SETTING:UPDATE";
/*------ start: CUSTOMER_MANAGEMENT------*/
public static final String CUSTOMER_MANAGEMENT_READ = "CUSTOMER_MANAGEMENT:READ";
public static final String CUSTOMER_MANAGEMENT_ADD = "CUSTOMER_MANAGEMENT:ADD";
public static final String CUSTOMER_MANAGEMENT_UPDATE = "CUSTOMER_MANAGEMENT:UPDATE";
public static final String CUSTOMER_MANAGEMENT_TRANSFER = "CUSTOMER_MANAGEMENT:TRANSFER";
public static final String CUSTOMER_MANAGEMENT_RECYCLE = "CUSTOMER_MANAGEMENT:RECYCLE";
public static final String CUSTOMER_MANAGEMENT_DELETE = "CUSTOMER_MANAGEMENT:DELETE";
public static final String CUSTOMER_MANAGEMENT_EXPORT = "CUSTOMER_MANAGEMENT:EXPORT";
public static final String CUSTOMER_MANAGEMENT_IMPORT = "CUSTOMER_MANAGEMENT:IMPORT";
public static final String CUSTOMER_MANAGEMENT_MERGE = "CUSTOMER_MANAGEMENT:MERGE";
/*------ end: CUSTOMER_MANAGEMENT ------*/
/*------ start: CUSTOMER_MANAGEMENT_POOL ------*/
public static final String CUSTOMER_MANAGEMENT_POOL_READ = "CUSTOMER_MANAGEMENT_POOL:READ";
public static final String CUSTOMER_MANAGEMENT_POOL_UPDATE = "CUSTOMER_MANAGEMENT_POOL:UPDATE";
public static final String CUSTOMER_MANAGEMENT_POOL_DELETE = "CUSTOMER_MANAGEMENT_POOL:DELETE";
public static final String CUSTOMER_MANAGEMENT_POOL_PICK = "CUSTOMER_MANAGEMENT_POOL:PICK";
public static final String CUSTOMER_MANAGEMENT_POOL_ASSIGN = "CUSTOMER_MANAGEMENT_POOL:ASSIGN";
public static final String CUSTOMER_MANAGEMENT_POOL_EXPORT = "CUSTOMER_MANAGEMENT_POOL:EXPORT";
/*------ end: CUSTOMER_MANAGEMENT_POOL ------*/
/*------ start: CUSTOMER_MANAGEMENT_CONTACT ------*/
public static final String CUSTOMER_MANAGEMENT_CONTACT_READ = "CUSTOMER_MANAGEMENT_CONTACT:READ";
public static final String CUSTOMER_MANAGEMENT_CONTACT_ADD = "CUSTOMER_MANAGEMENT_CONTACT:ADD";
public static final String CUSTOMER_MANAGEMENT_CONTACT_UPDATE = "CUSTOMER_MANAGEMENT_CONTACT:UPDATE";
public static final String CUSTOMER_MANAGEMENT_CONTACT_DELETE = "CUSTOMER_MANAGEMENT_CONTACT:DELETE";
public static final String CUSTOMER_MANAGEMENT_CONTACT_EXPORT = "CUSTOMER_MANAGEMENT_CONTACT:EXPORT";
public static final String CUSTOMER_MANAGEMENT_CONTACT_IMPORT = "CUSTOMER_MANAGEMENT_CONTACT:IMPORT";
/*------ end: CUSTOMER_MANAGEMENT_CONTACT ------*/
/*------ start: PRODUCT_MANAGEMENT ------*/
public static final String PRODUCT_MANAGEMENT_READ = "PRODUCT_MANAGEMENT:READ";
public static final String PRODUCT_MANAGEMENT_ADD = "PRODUCT_MANAGEMENT:ADD";
public static final String PRODUCT_MANAGEMENT_UPDATE = "PRODUCT_MANAGEMENT:UPDATE";
public static final String PRODUCT_MANAGEMENT_DELETE = "PRODUCT_MANAGEMENT:DELETE";
public static final String PRODUCT_MANAGEMENT_IMPORT = "PRODUCT_MANAGEMENT:IMPORT";
/*------ end: PRODUCT_MANAGEMENT ------*/
/*------ start: OPPORTUNITY_MANAGEMENT ------*/
public static final String OPPORTUNITY_MANAGEMENT_READ = "OPPORTUNITY_MANAGEMENT:READ";
public static final String OPPORTUNITY_MANAGEMENT_ADD = "OPPORTUNITY_MANAGEMENT:ADD";
public static final String OPPORTUNITY_MANAGEMENT_UPDATE = "OPPORTUNITY_MANAGEMENT:UPDATE";
public static final String OPPORTUNITY_MANAGEMENT_TRANSFER = "OPPORTUNITY_MANAGEMENT:TRANSFER";
public static final String OPPORTUNITY_MANAGEMENT_DELETE = "OPPORTUNITY_MANAGEMENT:DELETE";
public static final String OPPORTUNITY_MANAGEMENT_EXPORT = "OPPORTUNITY_MANAGEMENT:EXPORT";
public static final String OPPORTUNITY_MANAGEMENT_RESIGN = "OPPORTUNITY_MANAGEMENT:RESIGN";
public static final String OPPORTUNITY_MANAGEMENT_IMPORT = "OPPORTUNITY_MANAGEMENT:IMPORT";
/*------ end: OPPORTUNITY_MANAGEMENT ------*/
/**
* clue permission
*/
/*------ start: CLUE_MANAGEMENT ------*/
public static final String CLUE_MANAGEMENT_READ = "CLUE_MANAGEMENT:READ";
public static final String CLUE_MANAGEMENT_ADD = "CLUE_MANAGEMENT:ADD";
public static final String CLUE_MANAGEMENT_UPDATE = "CLUE_MANAGEMENT:UPDATE";
public static final String CLUE_MANAGEMENT_TRANSFER = "CLUE_MANAGEMENT:TRANSFER";
public static final String CLUE_MANAGEMENT_RECYCLE = "CLUE_MANAGEMENT:RECYCLE";
public static final String CLUE_MANAGEMENT_DELETE = "CLUE_MANAGEMENT:DELETE";
public static final String CLUE_MANAGEMENT_EXPORT = "CLUE_MANAGEMENT:EXPORT";
public static final String CLUE_MANAGEMENT_IMPORT = "CLUE_MANAGEMENT:IMPORT";
/*------ end: CLUE_MANAGEMENT ------*/
/*------ start: CLUE_MANAGEMENT_POOL ------*/
public static final String CLUE_MANAGEMENT_POOL_READ = "CLUE_MANAGEMENT_POOL:READ";
public static final String CLUE_MANAGEMENT_POOL_DELETE = "CLUE_MANAGEMENT_POOL:DELETE";
public static final String CLUE_MANAGEMENT_POOL_PICK = "CLUE_MANAGEMENT_POOL:PICK";
public static final String CLUE_MANAGEMENT_POOL_ASSIGN = "CLUE_MANAGEMENT_POOL:ASSIGN";
public static final String CLUE_MANAGEMENT_POOL_UPDATE = "CLUE_MANAGEMENT_POOL:UPDATE";
public static final String CLUE_MANAGEMENT_POOL_EXPORT = "CLUE_MANAGEMENT_POOL:EXPORT";
/*------ end: CLUE_MANAGEMENT_POOL ------*/
/**
* dashboard permission
*/
public static final String DASHBOARD_READ = "DASHBOARD:READ";
public static final String DASHBOARD_ADD = "DASHBOARD:ADD";
public static final String DASHBOARD_EDIT = "DASHBOARD:UPDATE";
public static final String DASHBOARD_DELETE = "DASHBOARD:DELETE";
/*------ start: LICENSE ------*/
public static final String LICENSE_READ = "LICENSE:READ";
public static final String LICENSE_EDIT = "LICENSE:EDIT";
/*------ end: LICENSE ------*/
/*------ start: PERSON INFO ------*/
public static final String PERSONAL_API_KEY_READ = "PERSONAL_API_KEY:READ";
public static final String PERSONAL_API_KEY_ADD = "PERSONAL_API_KEY:ADD";
public static final String PERSONAL_API_KEY_UPDATE = "PERSONAL_API_KEY:UPDATE";
public static final String PERSONAL_API_KEY_DELETE = "PERSONAL_API_KEY:DELETE";
/*------ end: PERSON INFO ------*/
/*------ start: AGENT ------*/
public static final String AGENT_READ = "AGENT:READ";
public static final String AGENT_ADD = "AGENT:ADD";
public static final String AGENT_UPDATE = "AGENT:UPDATE";
public static final String AGENT_DELETE = "AGENT:DELETE";
/*------ end: AGENT ------*/
/**
* product price permission
*/
public static final String PRICE_READ = "PRICE:READ";
public static final String PRICE_ADD = "PRICE:ADD";
public static final String PRICE_UPDATE = "PRICE:UPDATE";
public static final String PRICE_DELETE = "PRICE:DELETE";
public static final String PRICE_IMPORT = "PRICE:IMPORT";
public static final String PRICE_EXPORT = "PRICE:EXPORT";
/*------ start: OPPORTUNITY_QUOTATION ------*/
public static final String OPPORTUNITY_QUOTATION_READ = "OPPORTUNITY_QUOTATION:READ";
public static final String OPPORTUNITY_QUOTATION_ADD = "OPPORTUNITY_QUOTATION:ADD";
public static final String OPPORTUNITY_QUOTATION_UPDATE = "OPPORTUNITY_QUOTATION:UPDATE";
public static final String OPPORTUNITY_QUOTATION_DELETE = "OPPORTUNITY_QUOTATION:DELETE";
public static final String OPPORTUNITY_QUOTATION_DOWNLOAD = "OPPORTUNITY_QUOTATION:DOWNLOAD";
public static final String OPPORTUNITY_QUOTATION_VOIDED = "OPPORTUNITY_QUOTATION:VOIDED";
public static final String OPPORTUNITY_QUOTATION_APPROVAL = "OPPORTUNITY_QUOTATION:APPROVAL";
/*------ end: OPPORTUNITY_QUOTATION ------*/
/*------ start: CONTRACT ------*/
public static final String CONTRACT_READ = "CONTRACT:READ";
public static final String CONTRACT_ADD = "CONTRACT:ADD";
public static final String CONTRACT_UPDATE = "CONTRACT:UPDATE";
public static final String CONTRACT_DELETE = "CONTRACT:DELETE";
public static final String CONTRACT_EXPORT = "CONTRACT:EXPORT";
public static final String CONTRACT_APPROVAL = "CONTRACT:APPROVAL";
public static final String CONTRACT_STAGE = "CONTRACT:STAGE";
public static final String CONTRACT_PAYMENT = "CONTRACT:PAYMENT";
/*------ end: CONTRACT ------*/
/*------ start: CONTRACT_CONTRACT_PAYMENT_PLAN_ROLE ------*/
public static final String CONTRACT_PAYMENT_PLAN_READ = "CONTRACT_PAYMENT_PLAN:READ";
public static final String CONTRACT_PAYMENT_PLAN_ADD = "CONTRACT_PAYMENT_PLAN:ADD";
public static final String CONTRACT_PAYMENT_PLAN_UPDATE = "CONTRACT_PAYMENT_PLAN:UPDATE";
public static final String CONTRACT_PAYMENT_PLAN_DELETE = "CONTRACT_PAYMENT_PLAN:DELETE";
/*------ end: CONTRACT_CONTRACT_PAYMENT_PLAN_ROLE ------*/
/*------ start: TENDER ------*/
public static final String TENDER_READ = "TENDER:READ";
/*------ end: TENDER ------*/
/*------ start: CONTRACT_INVOICE_ROLE ------*/
public static final String CONTRACT_INVOICE_READ = "CONTRACT_INVOICE:READ";
public static final String CONTRACT_INVOICE_ADD = "CONTRACT_INVOICE:ADD";
public static final String CONTRACT_INVOICE_UPDATE = "CONTRACT_INVOICE:UPDATE";
public static final String CONTRACT_INVOICE_EXPORT = "CONTRACT_INVOICE:EXPORT";
public static final String CONTRACT_INVOICE_APPROVAL = "CONTRACT_INVOICE:APPROVAL";
public static final String CONTRACT_INVOICE_DELETE = "CONTRACT_INVOICE:DELETE";
/*------ end: CONTRACT_INVOICE_ROLE ------*/
/*------ start: BUSINESS_TITLE ------*/
public static final String CONTRACT_BUSINESS_TITLE_READ = "CONTRACT_BUSINESS_TITLE:READ";
public static final String CONTRACT_BUSINESS_TITLE_ADD = "CONTRACT_BUSINESS_TITLE:ADD";
public static final String CONTRACT_BUSINESS_TITLE_UPDATE = "CONTRACT_BUSINESS_TITLE:UPDATE";
public static final String CONTRACT_BUSINESS_TITLE_DELETE = "CONTRACT_BUSINESS_TITLE:DELETE";
public static final String CONTRACT_BUSINESS_TITLE_EXPORT = "CONTRACT_BUSINESS_TITLE:EXPORT";
public static final String CONTRACT_BUSINESS_TITLE_APPROVAL = "CONTRACT_BUSINESS_TITLE:APPROVAL";
public static final String CONTRACT_BUSINESS_TITLE_IMPORT = "CONTRACT_BUSINESS_TITLE:IMPORT";
/*------ end: BUSINESS_TITLE ------*/
/**
* Contract payment record permission
*/
public static final String CONTRACT_PAYMENT_RECORD_READ = "CONTRACT_PAYMENT_RECORD:READ";
public static final String CONTRACT_PAYMENT_RECORD_ADD = "CONTRACT_PAYMENT_RECORD:ADD";
public static final String CONTRACT_PAYMENT_RECORD_UPDATE = "CONTRACT_PAYMENT_RECORD:UPDATE";
public static final String CONTRACT_PAYMENT_RECORD_DELETE = "CONTRACT_PAYMENT_RECORD:DELETE";
public static final String CONTRACT_PAYMENT_RECORD_IMPORT = "CONTRACT_PAYMENT_RECORD:IMPORT";
public static final String CONTRACT_PAYMENT_RECORD_EXPORT = "CONTRACT_PAYMENT_RECORD:EXPORT";
/*------ start: ORDER_ROLE ------*/
public static final String ORDER_READ = "ORDER:READ";
public static final String ORDER_ADD = "ORDER:ADD";
public static final String ORDER_UPDATE = "ORDER:UPDATE";
public static final String ORDER_DELETE = "ORDER:DELETE";
public static final String ORDER_DOWNLOAD = "ORDER:DOWNLOAD";
/*------ end: ORDER_ROLE ------*/
/*------ start: PROCESS_SETTING ------*/
public static final String PROCESS_SETTING_READ = "PROCESS_SETTING:READ";
public static final String PROCESS_SETTING_ADD = "PROCESS_SETTING:ADD";
public static final String PROCESS_SETTING_UPDATE = "PROCESS_SETTING:UPDATE";
public static final String PROCESS_SETTING_DELETE = "PROCESS_SETTING:DELETE";
/*------ end: PROCESS_SETTING ------*/
/*------ start: CUSTOM_FORM ------*/
public static final String CUSTOM_FORM_READ = "CUSTOM_FORM:READ";
public static final String CUSTOM_FORM_ADD = "CUSTOM_FORM:ADD";
/*------ end: CUSTOM_FORM ------*/
}

View File

@@ -0,0 +1,26 @@
package cn.cordys.common.constants;
/**
* 角色的数据权限范围
*
* @Author: jianxing
* @CreateTime: 2025-01-07 16:42
*/
public enum RoleDataScope {
/**
* 全部数据权限
*/
ALL,
/**
* 指定部门数据权限
*/
DEPT_CUSTOM,
/**
* 部门及以下数据权限
*/
DEPT_AND_CHILD,
/**
* 仅本人数据权限
*/
SELF
}

View File

@@ -0,0 +1,13 @@
package cn.cordys.common.constants;
public class RuleValidatorConstants {
/**
* 必填
*/
public static final String REQUIRED = "required";
/**
* 唯一
*/
public static final String UNIQUE = "unique";
}

View File

@@ -0,0 +1,53 @@
package cn.cordys.common.constants;
/**
* 部门来源类型
*/
public enum ThirdConfigTypeConstants {
/**
* 本地
*/
INTERNAL,
/**
* 企业微信
*/
WECOM,
/**
* 钉钉
*/
DINGTALK,
/**
* 飞书
*/
LARK,
/**
* DE
*/
DE,
/**
* SQLBOT
*/
SQLBOT,
/**
* maxKB
*/
MAXKB,
/**
* tender
*/
TENDER,
/**
* 企查查
*/
QCC;
public static ThirdConfigTypeConstants fromString(String type) {
try {
return ThirdConfigTypeConstants.valueOf(type.toUpperCase());
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,5 @@
package cn.cordys.common.constants;
public enum ThirdDetailType {
WECOM_SYNC, DINGTALK_SYNC, LARK_SYNC, DE_BOARD, SQLBOT_CHAT, SQLBOT_BOARD, MAXKB, TENDER, QCC
}

View File

@@ -0,0 +1,25 @@
package cn.cordys.common.constants;
import java.util.List;
public class TopicConstants {
/**
* 下载任务的 Redis 主题名称
*/
public static final String DOWNLOAD_TOPIC = "download-topic";
/**
* sse 消息通知的Redis 主题名称
*/
public static final String SSE_TOPIC = "sse-topic";
/**
* 所有 Redis 主题的集合
* 用于统一管理订阅和发布的主题
*/
public static final List<String> ALL_TOPICS = List.of(DOWNLOAD_TOPIC, SSE_TOPIC);
private TopicConstants() {
// 私有构造函数,防止实例化
}
}

View File

@@ -0,0 +1,55 @@
package cn.cordys.common.constants;
/**
* 用户来源类型枚举类,用于标识用户的来源。
* <p>
* 此枚举类定义了不同的用户来源类型包括本地、LDAP、CAS、OIDC、OAuth2 和二维码。
* </p>
*/
public enum UserSource {
/**
* 本地用户来源,表示用户通过本地系统注册和登录。
*/
LOCAL,
/**
* LDAP 用户来源,表示用户通过 LDAP轻量目录访问协议系统认证。
*/
LDAP,
/**
* CAS 用户来源,表示用户通过 CAS中央认证服务认证。
*/
CAS,
/**
* OIDC 用户来源,表示用户通过 OIDC开放ID连接认证。
*/
OIDC,
/**
* OAUTH2 用户来源,表示用户通过 企业微信OAUTH2 授权框架认证。
*/
WECOM_OAUTH2,
/**
* OAUTH2 用户来源,表示用户通过 GitHub OAUTH2 授权框架认证。
*/
GITHUB_OAUTH2,
/**
* 二维码用户来源,表示用户通过扫描二维码登录。
*/
QR_CODE,
/**
* OAUTH2 用户来源,表示用户通过 钉钉OAUTH2 授权框架认证。
*/
DINGTALK_OAUTH2,
/**
* OAUTH2 用户来源,表示用户通过 飞书OAUTH2 授权框架认证。
*/
LARK_OAUTH2
}

View File

@@ -0,0 +1,6 @@
package cn.cordys.common.context;
@FunctionalInterface
public interface CustomFunction<T, R> {
R apply(T t) throws InterruptedException;
}

View File

@@ -0,0 +1,6 @@
package cn.cordys.common.context;
@FunctionalInterface
public interface ExportTaskFunction {
void apply() throws Exception;
}

View File

@@ -0,0 +1,42 @@
package cn.cordys.common.context;
import cn.cordys.context.OrganizationContext;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* 组织信息及请求来源的 Web 过滤器
* <p>
* 根据请求头自动设置组织上下文与请求来源,并在请求结束时清理资源。
*
* @author jianxing
*/
public class OrganizationContextWebFilter extends OncePerRequestFilter {
public static final String ORGANIZATION_ID_HEADER = "Organization-Id";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 提取所有头信息
String organizationId = request.getHeader(ORGANIZATION_ID_HEADER);
// 设置组织 ID
if (StringUtils.isNotBlank(organizationId)) {
OrganizationContext.setOrganizationId(organizationId);
}
try {
chain.doFilter(request, response);
} finally {
// 保证上下文清理,避免内存泄漏
OrganizationContext.clear();
}
}
}

View File

@@ -0,0 +1,68 @@
package cn.cordys.common.context;
import java.util.HashMap;
import java.util.Map;
/**
* 数据源详情解析上下文
*
* @author song-cc-rock
*/
public class SourceDetailResolveContext {
private static final ThreadLocal<Map<String, Map<String, Object>>> CONTEXT =
ThreadLocal.withInitial(HashMap::new);
private static final ThreadLocal<Integer> DEPTH =
ThreadLocal.withInitial(() -> 0);
public static Map<String, Map<String, Object>> getSourceMap() {
return CONTEXT.get();
}
public static boolean contains(String sourceId) {
return CONTEXT.get().containsKey(sourceId);
}
public static void putPlaceholder(String sourceId) {
CONTEXT.get().putIfAbsent(sourceId, new HashMap<>(8));
}
public static void put(String sourceId, Map<String, Object> detail) {
CONTEXT.get().put(sourceId, detail);
}
public static void start() {
DEPTH.set(DEPTH.get() + 1);
}
/**
* 获取当前深度
*
* @return 当前深度
*/
public static int getDepth() {
return DEPTH.get();
}
public static void end() {
int depth = DEPTH.get() - 1;
if (depth <= 0) {
clear();
DEPTH.remove();
} else {
DEPTH.set(depth);
}
}
public static void clear() {
CONTEXT.remove();
}
public static void remove(String sourceId) {
CONTEXT.get().remove(sourceId);
}
private SourceDetailResolveContext() {
}
}

View File

@@ -0,0 +1,24 @@
package cn.cordys.common.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class BaseModel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED)
private String id;
@Schema(description = "创建人")
private String createUser;
@Schema(description = "修改人")
private String updateUser;
@Schema(description = "创建时间")
private Long createTime;
@Schema(description = "更新时间")
private Long updateTime;
}

View File

@@ -0,0 +1,42 @@
package cn.cordys.common.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* @author jianxing
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BaseModuleFieldValue implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "自定义属性id")
private String fieldId;
/**
* 可能是数组
*/
@Schema(description = "自定义属性值")
private Object fieldValue;
public boolean valid() {
return switch (fieldValue) {
case null -> false;
case String fieldValueStr when StringUtils.isBlank(fieldValueStr) -> false;
case List fieldValueList when CollectionUtils.isEmpty(fieldValueList) -> false;
default -> true;
};
}
}

View File

@@ -0,0 +1,20 @@
package cn.cordys.common.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author jianxing
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BaseResourceField extends BaseModuleFieldValue {
@Schema(description = "ID")
private String id;
@Schema(description = "资源ID")
private String resourceId;
}

View File

@@ -0,0 +1,24 @@
package cn.cordys.common.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author song-cc-rock
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BaseResourceSubField extends BaseResourceField {
@Schema(description = "关联子表格ID")
private String refSubId;
@Schema(description = "行ID")
private String rowId;
@Schema(description = "行唯一标识")
private String bizId;
}

View File

@@ -0,0 +1,35 @@
package cn.cordys.common.dto;
import cn.cordys.common.dto.condition.BaseCondition;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import lombok.Data;
/**
* <p>表示分页请求的 DTO 类,继承自 {@link BaseCondition} 类,包含了分页参数和排序字段。</p>
* <p>用于分页查询时传递当前页码、每页条数和排序信息。</p>
*/
@Data
public class BasePageRequest extends BaseCondition {
/**
* 当前页码,最小值为 1
*/
@Min(value = 1, message = "当前页码必须大于0")
@Schema(description = "当前页码")
private int current;
/**
* 每页显示条数,范围为 1 到 500
*/
@Min(value = 1, message = "每页显示条数必须不小于1")
@Max(value = 500, message = "每页显示条数不能大于500")
@Schema(description = "每页显示条数")
private int pageSize;
@Valid
@Schema(description = "排序字段")
private SortRequest sort;
}

View File

@@ -0,0 +1,26 @@
package cn.cordys.common.dto;
import cn.cordys.common.util.JSON;
import lombok.Data;
import java.util.List;
/**
* @Author: jianxing
* @CreateTime: 2025-09-25 11:37
*/
@Data
public class BatchUpdateDbParam {
private List<String> ids;
private String fieldName;
private Object fieldValue;
private String updateUser;
private Long updateTime;
public Object getFieldValue() {
if (fieldValue != null && fieldValue instanceof List) {
return JSON.toJSONString(fieldValue);
}
return fieldValue;
}
}

View File

@@ -0,0 +1,11 @@
package cn.cordys.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class BusinessDataPermission extends DeptDataPermissionDTO {
@Schema(description = "数据来源表")
private String sourceTable;
}

View File

@@ -0,0 +1,33 @@
package cn.cordys.common.dto;
import cn.cordys.common.dto.chart.ChartCategoryAxisDbParam;
import cn.cordys.common.dto.chart.ChartValueAxisDbParam;
import cn.cordys.common.dto.condition.CombineSearch;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* @Author: jianxing
* @CreateTime: 2025-10-13 14:34
*/
@Data
public class ChartAnalysisDbRequest extends ChartAnalysisRequest {
@Schema(description = "过滤条件")
private CombineSearch viewFilterCondition;
/**
* x轴查询参数
*/
private ChartCategoryAxisDbParam categoryAxisParam;
/**
* x轴子类别查询参数
*/
private ChartCategoryAxisDbParam subCategoryAxisParam;
/**
* y轴查询参数
*/
private ChartValueAxisDbParam valueAxisParam;
}

View File

@@ -0,0 +1,28 @@
package cn.cordys.common.dto;
import cn.cordys.common.dto.chart.ChartConfig;
import cn.cordys.common.dto.condition.CombineSearch;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* @Author: jianxing
* @CreateTime: 2025-10-13 14:34
*/
@Data
public class ChartAnalysisRequest {
@Schema(description = "视图ID")
private String viewId;
@Schema(description = "过滤条件")
@Valid
private CombineSearch filterCondition;
@Schema(description = "搜索条件,支持组合搜索")
@NotNull
@Valid
private ChartConfig chartConfig;
}

View File

@@ -0,0 +1,38 @@
package cn.cordys.common.dto;
import cn.cordys.common.constants.InternalUserView;
import lombok.Data;
import java.util.HashSet;
import java.util.Set;
/**
* 部门的数据权限
*
* @author jianxing
*/
@Data
public class DeptDataPermissionDTO {
/**
* 搜索类型(ALL/SELF/DEPARTMENT/VISIBLE)
* {@link InternalUserView}
*/
private String viewId;
/**
* 是否可查看全部数据
*/
private Boolean all = false;
/**
* 是否可查看自己的数据
*/
private Boolean self = false;
/**
* 是否被设置为可见
*/
private Boolean visible = false;
/**
* 可查看的部门Id
*/
private Set<String> deptIds = new HashSet<>();
}

View File

@@ -0,0 +1,25 @@
package cn.cordys.common.dto;
import cn.cordys.common.domain.BaseModuleFieldValue;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.BooleanUtils;
/**
* @author jianxing
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EnableFieldValue extends BaseModuleFieldValue {
@Schema(description = "是否启用")
private Boolean enable;
public boolean valid() {
return super.valid() && BooleanUtils.isTrue(enable);
}
}

View File

@@ -0,0 +1,33 @@
package cn.cordys.common.dto;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Locale;
@Data
@Builder
public class ExportDTO {
private String userId;
private String orgId;
/**
* {@link cn.cordys.crm.system.constants.ExportConstants.ExportType}
*/
private String exportType;
private String logModule;
private Locale locale;
private String fileName;
private List<ExportHeadDTO> headList;
private DeptDataPermissionDTO deptDataPermission;
private BasePageRequest pageRequest;
private List<String> selectIds;
private ExportSelectRequest selectRequest;
private String formKey;
/**
* 导出字段参数 (通用参数无需设置)
*/
private ExportFieldParam exportFieldParam;
private List<String> mergeHeads;
private List<FieldExportMeta> exportMetas;
}

View File

@@ -0,0 +1,32 @@
package cn.cordys.common.dto;
import cn.cordys.crm.system.dto.field.base.BaseField;
import cn.cordys.crm.system.dto.response.ModuleFormConfigDTO;
import lombok.Builder;
import lombok.Data;
import java.util.Map;
import java.util.Set;
/**
* @author song-cc-rock
*/
@Data
@Builder
public class ExportFieldParam {
/**
* 子表格ID集合
*/
private Set<String> subIds;
/**
* 字段配置
*/
private Map<String, BaseField> fieldConfigMap;
/**
* 表单配置
*/
private ModuleFormConfigDTO formConfig;
}

View File

@@ -0,0 +1,19 @@
package cn.cordys.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ExportHeadDTO {
@Schema(description = "key")
private String key;
@Schema(description = "表头名称")
private String title;
@Schema(description = "字段类型")
private String columnType;
}

View File

@@ -0,0 +1,22 @@
package cn.cordys.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
@Data
public class ExportSelectRequest {
@Schema(description = "文件名")
private String fileName;
@Schema(description = "表头信息")
@NotEmpty(message = "{export_head_list_is_empty}")
private List<ExportHeadDTO> headList;
@Schema(description = "勾选的数据id集合")
@NotEmpty(message = "{export_select_ids_is_empty}")
private List<String> ids;
}

View File

@@ -0,0 +1,27 @@
package cn.cordys.common.dto;
import cn.cordys.common.resolver.field.AbstractModuleFieldResolver;
import cn.cordys.crm.system.dto.field.base.BaseField;
import lombok.Data;
/**
* 导出字段元数据 (预处理)
* @author song-cc-rock
*/
@Data
public class FieldExportMeta {
private String head;
private BaseField field;
private AbstractModuleFieldResolver<?> resolver;
private boolean noResource;
private String fieldId;
private String businessKey;
private String prefixId;
}

View File

@@ -0,0 +1,11 @@
package cn.cordys.common.dto;
import lombok.Data;
@Data
public class RedisMessage {
/**
* redis 发布订阅消息主体
*/
private String message;
}

View File

@@ -0,0 +1,33 @@
package cn.cordys.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 客户、商机、线索是否显示所有数据和部门数据 tab
*
* @Author: jianxing
* @CreateTime: 2025-05-15 14:54
*/
@Data
public class ResourceTabEnableDTO {
@Schema(description = "是否显示所有数据tab")
private Boolean all = false;
@Schema(description = "是否显示部门数据tab")
private Boolean dept = false;
/**
* 合并权限
*
* @param other 其余数据权限配置
*
* @return 合并后的数据权限配置
*/
public ResourceTabEnableDTO or(ResourceTabEnableDTO other) {
if (other != null) {
all |= other.all;
dept |= other.dept;
}
return this;
}
}

View File

@@ -0,0 +1,66 @@
package cn.cordys.common.dto;
import cn.cordys.common.utils.SqlInjectionChecker;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
/**
* @author jianxing
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SortRequest {
@Pattern(regexp = "^[A-Za-z0-9]+$")
@Schema(description = "排序字段")
private String name;
@Schema(description = "排序类型(asc/desc)")
private String type;
public static String camelToUnderline(String camelCase) {
if (camelCase == null || camelCase.isEmpty()) {
return camelCase;
}
// 使用正则表达式将驼峰转换为下划线
String underline = camelCase.replaceAll("([A-Z])", "_$1").toLowerCase();
// 如果开头有下划线,去掉
if (underline.startsWith("_")) {
underline = underline.substring(1);
}
return underline;
}
public String getName() {
if (SqlInjectionChecker.containsSqlInjectionRisk(name)) {
return "1";
}
return camelToUnderline(name);
}
public String getType() {
if (Strings.CI.equals(type, "asc")) {
return "asc";
} else {
return "desc";
}
}
/**
* mapper 中调用
*
* @return
*/
public boolean valid() {
return StringUtils.isNotBlank(name) && !SqlInjectionChecker.containsSqlInjectionRisk(name);
}
}

View File

@@ -0,0 +1,24 @@
package cn.cordys.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author jianxing
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDeptDTO {
@Schema(description = "用户ID")
private String userId;
@Schema(description = "部门ID")
private String deptId;
@Schema(description = "部门名称")
private String deptName;
}

View File

@@ -0,0 +1,16 @@
package cn.cordys.common.dto.chart;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* @Author: jianxing
* @CreateTime: 2025-10-13 14:54
*/
@Data
public class ChartCategoryAxisConfig {
@NotBlank
@Schema(description = "字段ID")
private String fieldId;
}

View File

@@ -0,0 +1,23 @@
package cn.cordys.common.dto.chart;
import lombok.Data;
/**
* @Author: jianxing
* @CreateTime: 2025-10-13 14:54
*/
@Data
public class ChartCategoryAxisDbParam extends ChartCategoryAxisConfig {
/**
* 是否要查blob表
*/
private Boolean blob = false;
/**
* 是否是业务字段
*/
private Boolean businessField = false;
/**
* 业务字段名称
*/
private String businessFieldName;
}

View File

@@ -0,0 +1,31 @@
package cn.cordys.common.dto.chart;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* @Author: jianxing
* @CreateTime: 2025-10-13 14:34
*/
@Data
public class ChartConfig {
@Schema(description = "图表类型")
private String chatType;
@Schema(description = "类别轴配置")
@NotNull
@Valid
private ChartCategoryAxisConfig categoryAxis;
@Schema(description = "子类别轴配置")
@Valid
private ChartCategoryAxisConfig subCategoryAxis;
@Schema(description = "值轴配置")
@NotNull
@Valid
private ChartValueAxisConfig valueAxis;
}

View File

@@ -0,0 +1,16 @@
package cn.cordys.common.dto.chart;
import lombok.Data;
/**
* @Author: jianxing
* @CreateTime: 2025-10-15 11:40
*/
@Data
public class ChartResult {
private String categoryAxis;
private String categoryAxisName;
private String subCategoryAxis;
private String subCategoryAxisName;
private Object valueAxis;
}

View File

@@ -0,0 +1,29 @@
package cn.cordys.common.dto.chart;
import cn.cordys.common.constants.ChartAggregateMethod;
import cn.cordys.common.constants.EnumValue;
import cn.cordys.common.uid.utils.EnumUtils;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* @Author: jianxing
* @CreateTime: 2025-10-13 14:54
*/
@Data
public class ChartValueAxisConfig {
@Schema(description = "字段ID")
private String fieldId;
@EnumValue(enumClass = ChartAggregateMethod.class)
@Schema(description = "聚合方式")
private String aggregateMethod;
public String getAggregateMethod() {
if (this.aggregateMethod == null) {
return ChartAggregateMethod.COUNT.name();
}
// 避免mapper中sql注入
return EnumUtils.valueOf(ChartAggregateMethod.class, this.aggregateMethod).name();
}
}

View File

@@ -0,0 +1,24 @@
package cn.cordys.common.dto.chart;
import lombok.Data;
/**
* @Author: jianxing
* @CreateTime: 2025-10-13 14:54
*/
@Data
public class ChartValueAxisDbParam extends ChartValueAxisConfig {
/**
* 是否要查blob表
*/
private Boolean blob = false;
/**
* 是否是业务字段
*/
private Boolean businessField = false;
/**
* 业务字段名称
*/
private String businessFieldName;
}

View File

@@ -0,0 +1,70 @@
package cn.cordys.common.dto.condition;
import cn.cordys.common.utils.ConditionFilterUtils;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.Data;
import org.apache.commons.lang3.Strings;
import java.util.List;
/**
* 表示 CRM 系统中的基础条件类,用于支持过滤和搜索操作。
*/
@Data
public class BaseCondition {
@Schema(description = "视图ID")
private String viewId;
@Schema(description = "关键字,用于搜索匹配")
private String keyword;
@Schema(description = "筛选条件列表,用于定义多个搜索条件")
@Valid
private List<FilterCondition> filters;
@Schema(description = "高级搜索条件,支持组合搜索")
@Valid
private CombineSearch combineSearch;
private CombineSearch viewCombineSearch;
/**
* 转义关键字中的特殊字符。
*
* @param keyword 输入的关键字
*
* @return 转义后的关键字
*/
public static String transferKeyword(String keyword) {
if (Strings.CS.contains(keyword, "\\") && !Strings.CS.contains(keyword, "\\\\")) {
keyword = Strings.CS.replace(keyword, "\\", "\\\\");
}
// 判断是否已经转义过,未转义才进行转义。
if (Strings.CS.contains(keyword, "%") && !Strings.CS.contains(keyword, "\\%")) {
keyword = Strings.CS.replace(keyword, "%", "\\%");
}
if (Strings.CS.contains(keyword, "_") && !Strings.CS.contains(keyword, "\\_")) {
keyword = Strings.CS.replace(keyword, "_", "\\_");
}
return keyword;
}
public CombineSearch getCombineSearch() {
return combineSearch == null ? new CombineSearch() : combineSearch;
}
public List<FilterCondition> getFilters() {
return ConditionFilterUtils.getValidConditions(filters);
}
/**
* 初始化关键字,直接设置字段值。
*
* @param keyword 初始化的关键字
*/
public void initKeyword(String keyword) {
this.keyword = keyword;
}
}

View File

@@ -0,0 +1,109 @@
package cn.cordys.common.dto.condition;
import cn.cordys.common.constants.EnumValue;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.Data;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
/**
* 表示组合搜索条件,用于支持复杂的搜索逻辑。
* 包含匹配模式(所有/任一)和筛选条件列表。
*/
@Data
public class CombineSearch {
@Schema(description = "匹配模式,支持“所有”或“任一”", allowableValues = {"AND", "OR"})
@EnumValue(enumClass = SearchMode.class)
private String searchMode = SearchMode.AND.name();
@Schema(description = "筛选条件列表,用于定义多个搜索条件")
@Valid
private List<FilterCondition> conditions;
public List<FilterCondition> getConditions() {
if (CollectionUtils.isEmpty(conditions)) {
return new ArrayList<>();
}
return conditions.stream()
.filter(FilterCondition::valid)
.collect(Collectors.toList());
}
/**
* 获取当前的匹配模式。如果未设置,则默认返回 "AND"。
*
* @return 当前的匹配模式
*/
public String getSearchMode() {
return StringUtils.isBlank(searchMode) ? SearchMode.AND.name() : searchMode;
}
public CombineSearch convert() {
if (CollectionUtils.isEmpty(conditions)) {
return this;
}
Iterator<FilterCondition> iterator = conditions.iterator();
while (iterator.hasNext()) {
FilterCondition condition = iterator.next();
if (!condition.valid()) {
iterator.remove();
continue;
}
Object value = condition.getCombineValue();
boolean isBetween = Strings.CS.equals(condition.getCombineOperator(), FilterCondition.CombineConditionOperator.BETWEEN.name());
if (value instanceof List<?> valueList) {
if (CollectionUtils.isEmpty(valueList)) {
/*
* 兜底处理, 防止前端[EMPTY, NOT_EMPTY]条件产生脏数据导致报错
*/
iterator.remove();
continue;
}
// 多值处理
if (!condition.expectMulti()) {
condition.setValue(valueList.getFirst());
}
if (isBetween) {
Object first = valueList.getFirst();
condition.setValue(List.of(first, first));
}
} else {
// 单值处理
if (condition.expectMulti()) {
if (isBetween) {
condition.setValue(List.of(value, value));
} else {
condition.setValue(List.of(value));
}
}
}
}
return this;
}
/**
* 枚举:搜索模式,定义了“所有”与“任一”两种匹配模式。
*/
public enum SearchMode {
/**
* 所有条件都匹配(“与”操作)
*/
AND,
/**
* 任一条件匹配(“或”操作)
*/
OR
}
}

View File

@@ -0,0 +1,472 @@
package cn.cordys.common.dto.condition;
import cn.cordys.common.constants.EnumValue;
import cn.cordys.common.exception.GenericException;
import cn.cordys.common.utils.SqlInjectionChecker;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import java.time.*;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.List;
/**
* 表示组合条件,用于支持复杂的过滤和查询逻辑。
* 包含字段名、操作符和期望值等信息。
*/
@Data
public class FilterCondition {
/**
* 系统字段为字段名
* 模块字段为字段ID
*/
@Schema(description = "条件的参数名称")
@NotNull
private String name;
@Schema(description = "期望值,若操作符为 BETWEEN, IN, NOT_IN 时为数组,其他操作符为单个值")
private Object value;
@Schema(description = "是否是多选值")
@NotNull
private Boolean multipleValue = false;
@Schema(description = "操作符",
allowableValues = {"IN", "NOT_IN", "BETWEEN", "GT", "LT", "GE", "LE", "COUNT_GT", "COUNT_LT", "EQUALS", "NOT_EQUALS", "CONTAINS", "NOT_CONTAINS", "EMPTY", "NOT_EMPTY"})
@EnumValue(enumClass = CombineConditionOperator.class)
private String operator;
@Schema(description = "类型")
private String type;
@Schema(description = "包含新增子部门集合")
private List<String> containChildIds;
public String getName() {
if (SqlInjectionChecker.containsSqlInjectionRisk(name)) {
throw new GenericException("condition name illegal");
}
return name;
}
/**
* 校验条件是否合法,检查字段名称、操作符和值的有效性。
*
* @return 如果条件合法则返回 true否则返回 false
*/
public boolean valid() {
if (StringUtils.isBlank(name) || StringUtils.isBlank(operator) || SqlInjectionChecker.containsSqlInjectionRisk(name)) {
return false;
}
// 针对空值判断操作符
if (Strings.CS.equalsAny(operator, CombineConditionOperator.EMPTY.name(), CombineConditionOperator.NOT_EMPTY.name(), CombineConditionOperator.NOT_EQUAL_ORIGINAL.name())) {
return true;
}
if (value == null) {
return false;
}
// 针对值为集合类型的校验
if (value instanceof List<?> valueList && CollectionUtils.isEmpty(valueList)) {
return false;
}
// 针对值为字符串的校验
return !(value instanceof String valueStr) || !StringUtils.isBlank(valueStr);
}
public boolean expectMulti() {
return Strings.CS.equalsAny(operator, CombineConditionOperator.IN.name(), CombineConditionOperator.NOT_IN.name(), CombineConditionOperator.BETWEEN.name(), CombineConditionOperator.DYNAMICS.name());
}
public Object getCombineValue() {
if (Strings.CI.equals(operator, CombineConditionOperator.DYNAMICS.name())) {
// value 转为string 类型
String strValue = (String) value;
String[] split = strValue.split(",");
if (split.length == 1) {
String dateValue = split[0];
switch (dateValue) {
case "TODAY" -> {
List<Long> todayList = new ArrayList<>();
// 获取今天的日期
LocalDate today = LocalDate.now();
long timestamp = getTimestamp(today.atStartOfDay());
todayList.add(timestamp);
long timestampEnd = getTimestamp(today.atTime(23, 59, 59, 999_000_000));
todayList.add(timestampEnd);
return todayList;
}
case "YESTERDAY" -> {
List<Long> yesterdayList = new ArrayList<>();
LocalDate yesterday = LocalDate.now().minusDays(1);
long timestamp = getTimestamp(yesterday.atStartOfDay());
yesterdayList.add(timestamp);
long timestampEnd = getTimestamp(yesterday.atTime(23, 59, 59, 999_000_000));
yesterdayList.add(timestampEnd);
return yesterdayList;
}
case "TOMORROW" -> {
List<Long> tomorrowList = new ArrayList<>();
LocalDate tomorrow = LocalDate.now().plusDays(1);
long timestamp = getTimestamp(tomorrow.atStartOfDay());
tomorrowList.add(timestamp);
long timestampEnd = getTimestamp(tomorrow.atTime(23, 59, 59, 999_000_000));
tomorrowList.add(timestampEnd);
return tomorrowList;
}
case "WEEK" -> {
List<Long> weeks = new ArrayList<>();
LocalDate startOfWeek = LocalDate.now().with(java.time.DayOfWeek.MONDAY);
long timestamp = getTimestamp(startOfWeek.atStartOfDay());
weeks.add(timestamp);
LocalDate now = LocalDate.now().with(DayOfWeek.SUNDAY);
long timestampEnd = getTimestamp(now.atTime(23, 59, 59, 999_000_000));
weeks.add(timestampEnd);
return weeks;
}
case "LAST_WEEK" -> {
List<Long> lastWeeks = new ArrayList<>();
LocalDate startOfLastWeek = LocalDate.now().minusWeeks(1).with(java.time.DayOfWeek.MONDAY);
long timestamp = getTimestamp(startOfLastWeek.atStartOfDay());
lastWeeks.add(timestamp);
LocalDate startOfLastWeekEnd = LocalDate.now().minusWeeks(1).with(DayOfWeek.SUNDAY);
long timestampEnd = getTimestamp(startOfLastWeekEnd.atTime(23, 59, 59, 999_000_000));
lastWeeks.add(timestampEnd);
return lastWeeks;
}
case "NEXT_WEEK" -> {
List<Long> nextWeeks = new ArrayList<>();
LocalDate startOfNextWeek = LocalDate.now().plusWeeks(1).with(java.time.DayOfWeek.MONDAY);
long timestamp = getTimestamp(startOfNextWeek.atStartOfDay());
nextWeeks.add(timestamp);
LocalDate startOfNextWeekEnd = LocalDate.now().plusWeeks(1).with(DayOfWeek.SUNDAY);
long timestampEnd = getTimestamp(startOfNextWeekEnd.atTime(23, 59, 59, 999_000_000));
nextWeeks.add(timestampEnd);
return nextWeeks;
}
case "MONTH" -> {
List<Long> months = new ArrayList<>();
LocalDate startOfMonth = LocalDate.now().withDayOfMonth(1);
long timestamp = getTimestamp(startOfMonth.atStartOfDay());
months.add(timestamp);
LocalDate now = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
long timestampEnd = getTimestamp(now.atTime(23, 59, 59, 999_000_000));
months.add(timestampEnd);
return months;
}
case "LAST_MONTH" -> {
List<Long> lastMonths = new ArrayList<>();
LocalDate startOfLastMonth = LocalDate.now().minusMonths(1).withDayOfMonth(1);
long timestamp = getTimestamp(startOfLastMonth.atStartOfDay());
lastMonths.add(timestamp);
LocalDate startOfLastMonthEnd = LocalDate.now().minusMonths(1).with(TemporalAdjusters.lastDayOfMonth());
long timestampEnd = getTimestamp(startOfLastMonthEnd.atTime(23, 59, 59, 999_000_000));
lastMonths.add(timestampEnd);
return lastMonths;
}
case "NEXT_MONTH" -> {
List<Long> nextMonths = new ArrayList<>();
LocalDate startOfNextMonth = LocalDate.now().plusMonths(1).withDayOfMonth(1);
long timestamp = getTimestamp(startOfNextMonth.atStartOfDay());
nextMonths.add(timestamp);
LocalDate startOfNextMonthEnd = LocalDate.now().plusMonths(1).with(TemporalAdjusters.lastDayOfMonth());
long timestampEnd = getTimestamp(startOfNextMonthEnd.atTime(23, 59, 59, 999_000_000));
nextMonths.add(timestampEnd);
return nextMonths;
}
case "LAST_SEVEN" -> {
List<Long> lastSevens = new ArrayList<>();
LocalDate startOfLastSevenDays = LocalDate.now().minusDays(7);
long timestamp = getTimestamp(startOfLastSevenDays.atStartOfDay());
lastSevens.add(timestamp);
long timestampEnd = getTimestamp(LocalDate.now().atStartOfDay());
lastSevens.add(timestampEnd);
return lastSevens;
}
case "SEVEN" -> {
List<Long> sevens = new ArrayList<>();
LocalDate startOfNextSevenDays = LocalDate.now().plusDays(6);
long timestamp = getTimestamp(LocalDate.now().atStartOfDay());
sevens.add(timestamp);
long timestampEnd = getTimestamp(startOfNextSevenDays.atTime(23, 59, 59, 999_000_000));
sevens.add(timestampEnd);
return sevens;
}
case "THIRTY" -> {
List<Long> thirty = new ArrayList<>();
LocalDate startOfNextThirtyDays = LocalDate.now().plusDays(29);
long timestamp = getTimestamp(LocalDate.now().atStartOfDay());
thirty.add(timestamp);
long timestampEnd = getTimestamp(startOfNextThirtyDays.atTime(23, 59, 59, 999_000_000));
thirty.add(timestampEnd);
return thirty;
}
case "LAST_THIRTY" -> {
List<Long> lastThirty = new ArrayList<>();
LocalDate startOfLastThirtyDays = LocalDate.now().minusDays(30);
long timestamp = getTimestamp(startOfLastThirtyDays.atStartOfDay());
lastThirty.add(timestamp);
long timestampEnd = getTimestamp(LocalDate.now().atStartOfDay());
lastThirty.add(timestampEnd);
return lastThirty;
}
case "SIXTY" -> {
List<Long> sixty = new ArrayList<>();
LocalDate startOfNextSixtyDays = LocalDate.now().plusDays(59);
long timestamp = getTimestamp(LocalDate.now().atStartOfDay());
sixty.add(timestamp);
long timestampEnd = getTimestamp(startOfNextSixtyDays.atTime(23, 59, 59, 999_000_000));
sixty.add(timestampEnd);
return sixty;
}
case "LAST_SIXTY" -> {
List<Long> lastSixty = new ArrayList<>();
LocalDate startOfLastSixtyDays = LocalDate.now().minusDays(60);
long timestamp = getTimestamp(startOfLastSixtyDays.atStartOfDay());
lastSixty.add(timestamp);
long timestampEnd = getTimestamp(LocalDate.now().atStartOfDay());
lastSixty.add(timestampEnd);
return lastSixty;
}
//本季度
case "QUARTER" -> {
List<Long> quarters = new ArrayList<>();
LocalDate now = LocalDate.now();
int currentMonth = now.getMonthValue();
int startMonth = (currentMonth - 1) / 3 * 3 + 1;
LocalDate startOfQuarter = LocalDate.of(now.getYear(), startMonth, 1);
long timestamp = getTimestamp(startOfQuarter.atStartOfDay());
quarters.add(timestamp);
LocalDate endOfQuarter = startOfQuarter.plusMonths(2).with(TemporalAdjusters.lastDayOfMonth());
long timestampEnd = getTimestamp(endOfQuarter.atTime(23, 59, 59, 999_000_000));
quarters.add(timestampEnd);
return quarters;
}
//上季度
case "LAST_QUARTER" -> {
List<Long> lastQuarters = new ArrayList<>();
LocalDate now = LocalDate.now();
int currentMonth = now.getMonthValue();
int startMonth = (currentMonth - 1) / 3 * 3 + 1;
LocalDate startOfLastQuarter = LocalDate.of(now.getYear(), startMonth, 1).minusMonths(3);
long timestamp = getTimestamp(startOfLastQuarter.atStartOfDay());
lastQuarters.add(timestamp);
LocalDate endOfLastQuarter = startOfLastQuarter.plusMonths(2).with(TemporalAdjusters.lastDayOfMonth());
long timestampEnd = getTimestamp(endOfLastQuarter.atTime(23, 59, 59, 999_000_000));
lastQuarters.add(timestampEnd);
return lastQuarters;
}
//下季度
case "NEXT_QUARTER" -> {
List<Long> nextQuarters = new ArrayList<>();
LocalDate now = LocalDate.now();
int currentMonth = now.getMonthValue();
int startMonth = (currentMonth - 1) / 3 * 3 + 1;
LocalDate startOfNextQuarter = LocalDate.of(now.getYear(), startMonth, 1).plusMonths(3);
long timestamp = getTimestamp(startOfNextQuarter.atStartOfDay());
nextQuarters.add(timestamp);
LocalDate endOfNextQuarter = startOfNextQuarter.plusMonths(2).with(TemporalAdjusters.lastDayOfMonth());
long timestampEnd = getTimestamp(endOfNextQuarter.atTime(23, 59, 59, 999_000_000));
nextQuarters.add(timestampEnd);
return nextQuarters;
}
//本年度
case "YEAR" -> {
List<Long> years = new ArrayList<>();
LocalDate startOfYear = LocalDate.now().withDayOfYear(1);
long timestamp = getTimestamp(startOfYear.atStartOfDay());
years.add(timestamp);
LocalDate now = LocalDate.now().with(TemporalAdjusters.lastDayOfYear());
long timestampEnd = getTimestamp(now.atTime(23, 59, 59, 999_000_000));
years.add(timestampEnd);
return years;
}
//上年度
case "LAST_YEAR" -> {
List<Long> lastYears = new ArrayList<>();
LocalDate startOfLastYear = LocalDate.now().minusYears(1).withDayOfYear(1);
long timestamp = getTimestamp(startOfLastYear.atStartOfDay());
lastYears.add(timestamp);
LocalDate startOfLastYearEnd = LocalDate.now().minusYears(1).with(TemporalAdjusters.lastDayOfYear());
long timestampEnd = getTimestamp(startOfLastYearEnd.atTime(23, 59, 59, 999_000_000));
lastYears.add(timestampEnd);
return lastYears;
}
//下年度
case "NEXT_YEAR" -> {
List<Long> nextYears = new ArrayList<>();
LocalDate startOfNextYear = LocalDate.now().plusYears(1).withDayOfYear(1);
long timestamp = getTimestamp(startOfNextYear.atStartOfDay());
nextYears.add(timestamp);
LocalDate startOfNextYearEnd = LocalDate.now().plusYears(1).with(TemporalAdjusters.lastDayOfYear());
long timestampEnd = getTimestamp(startOfNextYearEnd.atTime(23, 59, 59, 999_000_000));
nextYears.add(timestampEnd);
return nextYears;
}
}
} else {
String dateValue = split[1];
String dateUnit = split[2];
int dateNumber = Integer.parseInt(dateValue);
switch (dateUnit) {
case "BEFORE_DAY" -> {
LocalDateTime startOfLastDays = LocalDateTime.now().minusDays(dateNumber);
return getTimestamp(startOfLastDays);
}
case "AFTER_DAY" -> {
LocalDateTime startOfNextDays = LocalDateTime.now().plusDays(dateNumber);
return getTimestamp(startOfNextDays);
}
case "BEFORE_WEEK" -> {
LocalDateTime startOfLastWeeks = LocalDateTime.now().minusDays(dateNumber * 7L);
return getTimestamp(startOfLastWeeks);
}
case "AFTER_WEEK" -> {
LocalDateTime startOfNextWeeks = LocalDateTime.now().plusDays(dateNumber * 7L);
return getTimestamp(startOfNextWeeks);
}
case "BEFORE_MONTH" -> {
LocalDateTime startOfLastMonths = LocalDateTime.now().minusMonths(dateNumber);
return getTimestamp(startOfLastMonths);
}
case "AFTER_MONTH" -> {
LocalDateTime startOfNextMonths = LocalDateTime.now().plusMonths(dateNumber);
return getTimestamp(startOfNextMonths);
}
}
}
}
return value;
}
public String getCombineOperator() {
if (Strings.CI.equals(operator, CombineConditionOperator.DYNAMICS.name())) {
String strValue = (String) value;
String[] split = strValue.split(",");
if (split.length == 1) {
return CombineConditionOperator.BETWEEN.name();
} else {
String dateUnit = split[2];
switch (dateUnit) {
case "BEFORE_DAY", "BEFORE_WEEK", "BEFORE_MONTH" -> {
return CombineConditionOperator.LT.name();
}
case "AFTER_DAY", "AFTER_WEEK", "AFTER_MONTH" -> {
return CombineConditionOperator.GT.name();
}
}
}
}
return operator;
}
private long getTimestamp(LocalDateTime today) {
// 使用系统默认时区
ZonedDateTime zonedEndOfDay = today.atZone(ZoneId.systemDefault());
// 转为时间戳(毫秒)
return zonedEndOfDay.toInstant().toEpochMilli();
}
/**
* 枚举:组合条件操作符,定义了各种可能的查询操作符。
*/
public enum CombineConditionOperator {
/**
* 动态
*/
DYNAMICS,
/**
* 属于某个集合
*/
IN,
/**
* 不属于某个集合
*/
NOT_IN,
/**
* 区间操作
*/
BETWEEN,
/**
* 大于
*/
GT,
/**
* 小于
*/
LT,
/**
* 大于等于
*/
GE,
/**
* 小于等于
*/
LE,
/**
* 数量大于
*/
COUNT_GT,
/**
* 数量小于
*/
COUNT_LT,
/**
* 等于
*/
EQUALS,
/**
* 不等于
*/
NOT_EQUALS,
/**
* 包含
*/
CONTAINS,
/**
* 不包含
*/
NOT_CONTAINS,
/**
* 为空
*/
EMPTY,
/**
* 不为空
*/
NOT_EMPTY,
/**
* 不等于原值(用户审批时的条件判断)
*/
NOT_EQUAL_ORIGINAL
}
}

View File

@@ -0,0 +1,30 @@
package cn.cordys.common.dto.condition;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 表示组合条件,用于支持复杂的过滤和查询逻辑。
* 包含字段名、操作符和期望值等信息。
*/
@Data
public class FilterDBCondition extends FilterCondition {
@Schema(description = "是否是自定义字段")
private Boolean customField = false;
@Schema(description = "是否是大字段")
private Boolean blob = false;
@Schema(description = "是否是显示字段")
private Boolean refFiled = false;
@Schema(description = "显示字段的主字段是否是自定义字段")
private Boolean refMainCustomField = false;
@Schema(description = "显示字段的主字段名称或ID")
private String refMainFieldName;
@Schema(description = "显示字段的主表名")
private String refMainTableName;
}

View File

@@ -0,0 +1,15 @@
package cn.cordys.common.dto.stage;
import cn.cordys.common.domain.BaseModuleFieldValue;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class CirculationFieldValue extends BaseModuleFieldValue {
@Schema(description = "是否必填")
private Boolean required;
@Schema(description = "默认值类型")
private String valueType;
}

View File

@@ -0,0 +1,19 @@
package cn.cordys.common.dto.stage;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class CirculationSetting {
@Schema(description = "源id")
private String originId;
@Schema(description = "源id对应的行目标ids")
private List<Target> targets;
@Schema(description = "模块类型(order-订单/contract-合同)")
private String moduleType;
}

View File

@@ -0,0 +1,20 @@
package cn.cordys.common.dto.stage;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class StageAddRequest {
@Schema(description = "")
private String name;
@Schema(description = "类型")
private String type;
@Schema(description = "添加的位置(取值:-1,1。 -1源节点之前1源节点之后", requiredMode = Schema.RequiredMode.REQUIRED)
private int dropPosition;
@Schema(description = "源节点")
private String targetId;
}

View File

@@ -0,0 +1,16 @@
package cn.cordys.common.dto.stage;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class StageAdvancedConfigRequest {
@Schema(description = "流转配置类型")
private String circulationType;
@Schema(description = "高级流转设置")
private List<CirculationSetting> circulationSettings;
}

View File

@@ -0,0 +1,32 @@
package cn.cordys.common.dto.stage;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class StageConfigResponse {
@Schema(description = "ID")
private String id;
@Schema(description = "状态")
private String name;
@Schema(description = "类型")
private String type;
@Schema(description = "进行中回退设置")
private Boolean afootRollBack;
@Schema(description = "完结回退设置")
private Boolean endRollBack;
@Schema(description = "顺序")
private Long pos;
@Schema(description = "当前阶段是否存在数据")
private Boolean stageHasData = false;
@Schema(description = "流转类型")
private String circulationType;
}

View File

@@ -0,0 +1,25 @@
package cn.cordys.common.dto.stage;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class StageConfigsResponse {
@Schema(description = "订单状态流配置列表")
List<StageConfigResponse> stageConfigList;
@Schema(description = "进行中回退设置")
private Boolean afootRollBack = true;
@Schema(description = "完结回退设置")
private Boolean endRollBack = false;
@Schema(description = "流转配置类型")
private String circulationType;
@Schema(description = "高级流转设置")
private List<CirculationSetting> advancedConfigs;
}

View File

@@ -0,0 +1,14 @@
package cn.cordys.common.dto.stage;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class StageRollBackRequest {
@Schema(description = "进行中回退设置")
private Boolean afootRollBack;
@Schema(description = "完结回退设置")
private Boolean endRollBack;
}

View File

@@ -0,0 +1,21 @@
package cn.cordys.common.dto.stage;
import cn.cordys.common.domain.BaseModuleFieldValue;
import cn.cordys.crm.system.dto.request.NodeMoveRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.util.List;
@Data
public class StageSortRequest extends NodeMoveRequest {
@NotBlank
@Schema(description = "阶段", requiredMode = Schema.RequiredMode.REQUIRED)
private String stage;
@Schema(description = "更新字段")
private List<BaseModuleFieldValue> fields;
}

View File

@@ -0,0 +1,16 @@
package cn.cordys.common.dto.stage;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class StageUpdateRequest {
@Schema(description = "id")
private String id;
@Schema(description = "状态名称")
private String name;
}

Some files were not shown because too many files have changed in this diff Show More