From 3411691fba52492dd417e4d7707c84f6250fff4f Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 12 八月 2026 16:18:50 +0800
Subject: [PATCH] feat(crm): 添加销售目标管理功能
---
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java | 3
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/salesTarget/CrmSalesTargetDimensionEnum.java | 45 +
.playwright-mcp/page-2026-08-12T08-14-30-909Z.yml | 1
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/CrmSalesTargetController.java | 104 ++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/CrmSalesTargetRespVO.java | 130 +++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java | 4
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/salesTarget/CrmSalesTargetMapper.java | 38 +
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/CrmSalesTargetPageReqVO.java | 30
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/salesTarget/CrmSalesTargetAchievementMapper.java | 62 +
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/salesTarget/CrmSalesTargetService.java | 81 ++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/salesTarget/CrmSalesTargetDO.java | 107 ++
yudao-module-crm/src/main/resources/mapper/salesTarget/CrmSalesTargetAchievementMapper.xml | 84 ++
.playwright-mcp/page-2026-08-12T08-14-46-176Z.yml | 194 +++++
.playwright-mcp/page-2026-08-12T08-16-45-289Z.yml | 215 +++++
docs/crm_sales_target_integration.md | 141 +++
docs/sql/crm_sales_target_init.sql | 75 ++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/CrmSalesTargetChangeStatusReqVO.java | 22
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/analysis/CrmSalesTargetAnalysisReqVO.java | 31
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/salesTarget/CrmSalesTargetServiceImpl.java | 368 ++++++++++
.playwright-mcp/page-2026-08-12T08-15-00-361Z.yml | 215 +++++
sales-target-analysis.png | 0
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/salesTarget/CrmSalesTargetPeriodTypeEnum.java | 45 +
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/CrmSalesTargetSaveReqVO.java | 65 +
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/salesTarget/CrmSalesTargetStatusEnum.java | 46 +
24 files changed, 2,106 insertions(+), 0 deletions(-)
diff --git a/.playwright-mcp/page-2026-08-12T08-14-30-909Z.yml b/.playwright-mcp/page-2026-08-12T08-14-30-909Z.yml
new file mode 100644
index 0000000..6adf37e
--- /dev/null
+++ b/.playwright-mcp/page-2026-08-12T08-14-30-909Z.yml
@@ -0,0 +1 @@
+- generic [active]: 宸ヤ笟AI鏅洪�犲钩鍙�
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-08-12T08-14-46-176Z.yml b/.playwright-mcp/page-2026-08-12T08-14-46-176Z.yml
new file mode 100644
index 0000000..c0aa8b9
--- /dev/null
+++ b/.playwright-mcp/page-2026-08-12T08-14-46-176Z.yml
@@ -0,0 +1,194 @@
+- generic [ref=e7]:
+ - complementary [ref=e8]:
+ - generic [ref=e9]:
+ - generic [ref=e10] [cursor=pointer]
+ - link "宸ヤ笟AI鏅洪�犲钩鍙� 宸ヤ笟AI鏅洪�犲钩鍙�" [ref=e15] [cursor=pointer]:
+ - /url: javascript:void 0
+ - img "宸ヤ笟AI鏅洪�犲钩鍙�" [ref=e18]
+ - generic [ref=e19]: 宸ヤ笟AI鏅洪�犲钩鍙�
+ - menu [ref=e22]:
+ - menuitem "棣栭〉" [ref=e23] [cursor=pointer]
+ - listitem [ref=e30]:
+ - generic [ref=e31] [cursor=pointer]: 鍩虹鏁版嵁
+ - listitem [ref=e39]:
+ - generic [ref=e40] [cursor=pointer]: 閲囪喘绠$悊
+ - listitem [ref=e46]:
+ - generic [ref=e47] [cursor=pointer]: 钀ラ攢绠$悊
+ - list [ref=e577]:
+ - menuitem "寰呭姙浜嬮」" [ref=e578] [cursor=pointer]
+ - menuitem "绾跨储绠$悊" [ref=e583] [cursor=pointer]
+ - menuitem "鍟嗘満绠$悊" [ref=e588] [cursor=pointer]
+ - menuitem "閿�鍞姤浠�" [ref=e593] [cursor=pointer]
+ - menuitem "鍚堝悓绠$悊" [ref=e599] [cursor=pointer]
+ - menuitem "閿�鍞鍗�" [ref=e605] [cursor=pointer]
+ - menuitem "瀹㈡埛绠$悊" [ref=e610] [cursor=pointer]
+ - menuitem "鑱旂郴浜虹鐞�" [ref=e615] [cursor=pointer]
+ - menuitem "瀹㈡埛鍏捣" [ref=e620] [cursor=pointer]
+ - menuitem "寮�绁ㄧ鐞�" [ref=e625] [cursor=pointer]
+ - menuitem "鍥炴绠$悊" [ref=e630] [cursor=pointer]
+ - menuitem "鍥炴璁″垝" [ref=e637] [cursor=pointer]
+ - menuitem "閿�鍞鍒�" [ref=e642] [cursor=pointer]
+ - menuitem "杈炬垚鍒嗘瀽" [ref=e647] [cursor=pointer]
+ - listitem [ref=e653]:
+ - generic [ref=e654] [cursor=pointer]: 鏁版嵁缁熻
+ - listitem [ref=e660]:
+ - generic [ref=e661] [cursor=pointer]: 绯荤粺閰嶇疆
+ - listitem [ref=e53]:
+ - generic [ref=e54] [cursor=pointer]: 璁惧绠$悊
+ - listitem [ref=e61]:
+ - generic [ref=e62] [cursor=pointer]: 浠撳偍鐗╂祦
+ - listitem [ref=e70]:
+ - generic [ref=e71] [cursor=pointer]: 鐢熶骇绠℃帶
+ - listitem [ref=e78]:
+ - generic [ref=e79] [cursor=pointer]: 璐ㄩ噺绠$悊
+ - listitem [ref=e85]:
+ - generic [ref=e86] [cursor=pointer]: 鍐崇瓥BI澶у睆
+ - listitem [ref=e92]:
+ - generic [ref=e93] [cursor=pointer]: 鍗忓悓鍔炲叕
+ - listitem [ref=e99]:
+ - generic [ref=e100] [cursor=pointer]: 浜哄姏璧勬簮
+ - listitem [ref=e106]:
+ - generic [ref=e107] [cursor=pointer]: 鍞悗绠$悊
+ - listitem [ref=e113]:
+ - generic [ref=e114] [cursor=pointer]: 鍩虹璁炬柦
+ - listitem [ref=e120]:
+ - generic [ref=e121] [cursor=pointer]: 绯荤粺绠$悊
+ - listitem [ref=e127]:
+ - generic [ref=e128] [cursor=pointer]: IM 鍗虫椂閫氳
+ - generic [ref=e136] [cursor=pointer]
+ - generic [ref=e141]:
+ - generic [ref=e142]:
+ - banner [ref=e143]:
+ - button [ref=e144] [cursor=pointer]
+ - button [ref=e147] [cursor=pointer]
+ - navigation "breadcrumb" [ref=e152]:
+ - list [ref=e153]:
+ - listitem [ref=e154]:
+ - link "棣栭〉" [disabled] [ref=e155]
+ - generic [ref=e162]:
+ - generic [ref=e164] [cursor=pointer]:
+ - generic [ref=e168]: 鎼滅储
+ - generic [ref=e169]: Ctrl K
+ - button [ref=e171] [cursor=pointer]
+ - button "dark" [ref=e176] [cursor=pointer]
+ - button [ref=e185] [cursor=pointer]
+ - button [ref=e191] [cursor=pointer]
+ - button [ref=e194] [cursor=pointer]
+ - button [ref=e200] [cursor=pointer]:
+ - button [ref=e202]
+ - button "宸ヤ綔鍙�" [ref=e206] [cursor=pointer]
+ - button [ref=e210] [cursor=pointer]:
+ - img "瓒呯骇绠$悊鍛�" [ref=e215]
+ - generic [ref=e217]:
+ - generic [ref=e218]: 棣栭〉
+ - generic [ref=e239]:
+ - button [ref=e240] [cursor=pointer]
+ - generic [ref=e247] [cursor=pointer]
+ - generic [ref=e251] [cursor=pointer]
+ - main [ref=e258]:
+ - generic [ref=e261]:
+ - generic [ref=e262]:
+ - generic [ref=e263]:
+ - heading "涓嬪崍濂斤紝娆㈣繋鍥炴潵" [level=1] [ref=e264]
+ - paragraph [ref=e265]: 2026骞�8鏈�12鏃� 鏄熸湡涓�
+ - generic [ref=e266]: 绯荤粺杩愯涓�
+ - generic [ref=e271]:
+ - generic [ref=e280] [cursor=pointer]:
+ - generic [ref=e281]: 鐢熶骇璁㈠崟
+ - generic [ref=e282]:
+ - generic [ref=e283]: "25"
+ - generic [ref=e285]: 杩涜涓�
+ - generic [ref=e286]:
+ - text: 寰呮帓浜� 15
+ - generic [ref=e288]: "|"
+ - text: 宸插畬鎴� 5
+ - generic [ref=e300] [cursor=pointer]:
+ - generic [ref=e301]: 浠婃棩浜ч噺
+ - generic [ref=e302]:
+ - generic [ref=e303]: "0"
+ - generic [ref=e305]: 浠�
+ - generic [ref=e306]: 鏄ㄦ棩 0 浠�
+ - generic [ref=e317] [cursor=pointer]:
+ - generic [ref=e318]: 璐ㄩ噺鍚堟牸鐜�
+ - generic [ref=e319]:
+ - generic [ref=e320]: "0.0"
+ - generic [ref=e322]: "%"
+ - generic [ref=e323]: 鍚堟牸 0 | 涓嶈壇 0
+ - generic [ref=e335] [cursor=pointer]:
+ - generic [ref=e336]: 璁惧杩愯鐜�
+ - generic [ref=e337]:
+ - generic [ref=e338]: "33.3"
+ - generic [ref=e340]: "%"
+ - generic [ref=e341]:
+ - generic [ref=e342]: 鍋滄満 1
+ - generic [ref=e343]: "|"
+ - generic [ref=e344]: 缁存姢 1
+ - generic [ref=e347]:
+ - generic [ref=e357] [cursor=pointer]:
+ - generic [ref=e358]: 閲囪喘璁㈠崟
+ - generic [ref=e359]:
+ - generic [ref=e360]: "10"
+ - generic [ref=e362]: 绗�
+ - generic [ref=e363]: ERP 閲囪喘绠$悊
+ - generic [ref=e374] [cursor=pointer]:
+ - generic [ref=e375]: 閿�鍞鍗�
+ - generic [ref=e376]:
+ - generic [ref=e377]: "22"
+ - generic [ref=e379]: 绗�
+ - generic [ref=e380]: ERP 閿�鍞鐞�
+ - generic [ref=e392] [cursor=pointer]:
+ - generic [ref=e393]: 鐗╂枡搴撳瓨
+ - generic [ref=e394]:
+ - generic [ref=e395]: "15"
+ - generic [ref=e397]: 鏉�
+ - generic [ref=e398]: MES 搴撳瓨鍙拌处
+ - generic [ref=e410] [cursor=pointer]:
+ - generic [ref=e411]: 鎴戠殑寰呭姙
+ - generic [ref=e412]:
+ - generic [ref=e413]: "1"
+ - generic [ref=e415]: 椤�
+ - generic [ref=e416]: BPM 寰呭鎵�
+ - generic [ref=e419]:
+ - generic [ref=e420]:
+ - heading "AI 鏅鸿兘鍒嗘瀽" [level=3] [ref=e421]
+ - generic [ref=e422]: 25 涓伐鍗曞彲鍒嗘瀽
+ - generic [ref=e423]:
+ - generic [ref=e431] [cursor=pointer]:
+ - generic [ref=e432]: AI 鐗╂枡棰勬祴
+ - generic [ref=e433]: 棰勬祴鐗╂枡闇�姹備笌鐭己椋庨櫓
+ - generic [ref=e442] [cursor=pointer]:
+ - generic [ref=e443]: AI 鏃堕暱棰勬祴
+ - generic [ref=e444]: 棰勬祴鍚勫伐搴忕敓浜ц�楁椂
+ - generic [ref=e451] [cursor=pointer]:
+ - generic [ref=e452]: AI 椋庨櫓棰勬祴
+ - generic [ref=e453]: 璇嗗埆娼滃湪鐢熶骇椋庨櫓骞剁粰鍑哄缓璁�
+ - generic [ref=e463] [cursor=pointer]:
+ - generic [ref=e464]: AI 浜や粯璇勪及
+ - generic [ref=e465]: 璇勪及璁㈠崟浜や粯鍑嗘椂鐜囦笌寤舵湡鍥犵礌
+ - generic [ref=e468]:
+ - heading "鐢熶骇瓒嬪娍锛堣繎7澶╋級" [level=3] [ref=e471]
+ - generic [ref=e476]:
+ - generic [ref=e477]:
+ - generic [ref=e483] [cursor=pointer]:
+ - generic [ref=e484]: 瀹夌伅鍛煎彨
+ - generic [ref=e485]:
+ - generic [ref=e486]: "0"
+ - generic [ref=e488]: 鏈缃�
+ - generic [ref=e494] [cursor=pointer]:
+ - generic [ref=e495]: 缁翠慨宸ュ崟
+ - generic [ref=e496]:
+ - generic [ref=e497]: "0"
+ - generic [ref=e499]: 寰呭鐞�
+ - heading "宸ュ崟鐘舵�佸垎甯�" [level=4] [ref=e501]
+ - generic [ref=e509]:
+ - heading "蹇嵎鎿嶄綔" [level=3] [ref=e510]
+ - generic [ref=e511]:
+ - generic [ref=e512] [cursor=pointer]: 鐢熶骇宸ュ崟
+ - generic [ref=e519] [cursor=pointer]: 鐢熶骇鎶ュ伐
+ - generic [ref=e526] [cursor=pointer]: 璐ㄦ绠$悊
+ - generic [ref=e534] [cursor=pointer]: 璁惧鐐规
+ - generic [ref=e541] [cursor=pointer]: 鐢熶骇鎺掔▼
+ - generic [ref=e548] [cursor=pointer]: 瀹夌伅鍛煎彨
+ - generic [ref=e553] [cursor=pointer]: 鐗╂枡搴撳瓨
+ - generic [ref=e561] [cursor=pointer]: 宸ヨ壓璺嚎
+ - generic [ref=e569] [cursor=pointer]: 鏁版嵁鎶ヨ〃
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-08-12T08-15-00-361Z.yml b/.playwright-mcp/page-2026-08-12T08-15-00-361Z.yml
new file mode 100644
index 0000000..6bf9c8e
--- /dev/null
+++ b/.playwright-mcp/page-2026-08-12T08-15-00-361Z.yml
@@ -0,0 +1,215 @@
+- generic [ref=e7]:
+ - complementary [ref=e8]:
+ - generic [ref=e9]:
+ - generic [ref=e10] [cursor=pointer]
+ - link "宸ヤ笟AI鏅洪�犲钩鍙� 宸ヤ笟AI鏅洪�犲钩鍙�" [ref=e15] [cursor=pointer]:
+ - /url: javascript:void 0
+ - img "宸ヤ笟AI鏅洪�犲钩鍙�" [ref=e18]
+ - generic [ref=e19]: 宸ヤ笟AI鏅洪�犲钩鍙�
+ - menu [ref=e22]:
+ - menuitem "棣栭〉" [ref=e23] [cursor=pointer]
+ - listitem [ref=e30]:
+ - generic [ref=e31] [cursor=pointer]: 鍩虹鏁版嵁
+ - listitem [ref=e39]:
+ - generic [ref=e40] [cursor=pointer]: 閲囪喘绠$悊
+ - listitem [ref=e46]:
+ - generic [ref=e47] [cursor=pointer]: 钀ラ攢绠$悊
+ - list [ref=e577]:
+ - menuitem "寰呭姙浜嬮」" [ref=e578] [cursor=pointer]
+ - menuitem "绾跨储绠$悊" [ref=e583] [cursor=pointer]
+ - menuitem "鍟嗘満绠$悊" [ref=e588] [cursor=pointer]
+ - menuitem "閿�鍞姤浠�" [ref=e593] [cursor=pointer]
+ - menuitem "鍚堝悓绠$悊" [ref=e599] [cursor=pointer]
+ - menuitem "閿�鍞鍗�" [ref=e605] [cursor=pointer]
+ - menuitem "瀹㈡埛绠$悊" [ref=e610] [cursor=pointer]
+ - menuitem "鑱旂郴浜虹鐞�" [ref=e615] [cursor=pointer]
+ - menuitem "瀹㈡埛鍏捣" [ref=e620] [cursor=pointer]
+ - menuitem "寮�绁ㄧ鐞�" [ref=e625] [cursor=pointer]
+ - menuitem "鍥炴绠$悊" [ref=e630] [cursor=pointer]
+ - menuitem "鍥炴璁″垝" [ref=e637] [cursor=pointer]
+ - menuitem "閿�鍞鍒�" [ref=e642] [cursor=pointer]
+ - menuitem "杈炬垚鍒嗘瀽" [active] [ref=e647] [cursor=pointer]
+ - listitem [ref=e653]:
+ - generic [ref=e654] [cursor=pointer]: 鏁版嵁缁熻
+ - listitem [ref=e660]:
+ - generic [ref=e661] [cursor=pointer]: 绯荤粺閰嶇疆
+ - listitem [ref=e53]:
+ - generic [ref=e54] [cursor=pointer]: 璁惧绠$悊
+ - listitem [ref=e61]:
+ - generic [ref=e62] [cursor=pointer]: 浠撳偍鐗╂祦
+ - listitem [ref=e70]:
+ - generic [ref=e71] [cursor=pointer]: 鐢熶骇绠℃帶
+ - listitem [ref=e78]:
+ - generic [ref=e79] [cursor=pointer]: 璐ㄩ噺绠$悊
+ - listitem [ref=e85]:
+ - generic [ref=e86] [cursor=pointer]: 鍐崇瓥BI澶у睆
+ - listitem [ref=e92]:
+ - generic [ref=e93] [cursor=pointer]: 鍗忓悓鍔炲叕
+ - listitem [ref=e99]:
+ - generic [ref=e100] [cursor=pointer]: 浜哄姏璧勬簮
+ - listitem [ref=e106]:
+ - generic [ref=e107] [cursor=pointer]: 鍞悗绠$悊
+ - listitem [ref=e113]:
+ - generic [ref=e114] [cursor=pointer]: 鍩虹璁炬柦
+ - listitem [ref=e120]:
+ - generic [ref=e121] [cursor=pointer]: 绯荤粺绠$悊
+ - listitem [ref=e127]:
+ - generic [ref=e128] [cursor=pointer]: IM 鍗虫椂閫氳
+ - generic [ref=e136] [cursor=pointer]
+ - generic [ref=e141]:
+ - generic [ref=e142]:
+ - banner [ref=e143]:
+ - button [ref=e144] [cursor=pointer]
+ - button [ref=e147] [cursor=pointer]
+ - navigation "breadcrumb" [ref=e152]:
+ - list [ref=e153]:
+ - listitem [ref=e668]:
+ - link "钀ラ攢绠$悊" [ref=e669] [cursor=pointer]:
+ - /url: javascript:void 0
+ - listitem [ref=e673]
+ - listitem [ref=e676]:
+ - link "杈炬垚鍒嗘瀽" [disabled] [ref=e677]
+ - generic [ref=e162]:
+ - generic [ref=e164] [cursor=pointer]:
+ - generic [ref=e168]: 鎼滅储
+ - generic [ref=e169]: Ctrl K
+ - button [ref=e171] [cursor=pointer]
+ - button "dark" [ref=e176] [cursor=pointer]
+ - button [ref=e185] [cursor=pointer]
+ - button [ref=e191] [cursor=pointer]
+ - button [ref=e194] [cursor=pointer]
+ - button [ref=e200] [cursor=pointer]:
+ - button [ref=e202]
+ - button "宸ヤ綔鍙�" [ref=e206] [cursor=pointer]
+ - button [ref=e210] [cursor=pointer]:
+ - img "瓒呯骇绠$悊鍛�" [ref=e215]
+ - generic [ref=e217]:
+ - generic [ref=e222]:
+ - generic [ref=e223] [cursor=pointer]: 棣栭〉
+ - generic [ref=e684] [cursor=pointer]: 杈炬垚鍒嗘瀽
+ - generic [ref=e239]:
+ - button [ref=e240] [cursor=pointer]
+ - generic [ref=e247] [cursor=pointer]
+ - generic [ref=e251] [cursor=pointer]
+ - main [ref=e258]:
+ - generic [ref=e705]:
+ - generic [ref=e707]:
+ - generic [ref=e708]:
+ - generic [ref=e709]:
+ - generic [ref=e710]: "*"
+ - text: 鍛ㄦ湡绫诲瀷
+ - generic [ref=e716] [cursor=pointer]:
+ - combobox "* 鍛ㄦ湡绫诲瀷" [ref=e718]
+ - generic "骞村害" [ref=e719]
+ - generic [ref=e720]:
+ - generic [ref=e721]:
+ - generic [ref=e722]: "*"
+ - text: 鍛ㄦ湡鍊�
+ - textbox "* 鍛ㄦ湡鍊�" [ref=e727]:
+ - /placeholder: 濡� 2026 / 2026-Q1 / 2026-07
+ - text: "2026"
+ - generic [ref=e728]:
+ - generic [ref=e729]: 鐩爣缁村害
+ - generic [ref=e735] [cursor=pointer]:
+ - combobox "鐩爣缁村害" [ref=e737]
+ - generic: 鍏ㄩ儴
+ - generic [ref=e738]:
+ - generic [ref=e739]: 鐘舵��
+ - generic [ref=e745] [cursor=pointer]:
+ - combobox "鐘舵��" [ref=e747]
+ - generic: 鍏ㄩ儴
+ - generic [ref=e748]:
+ - button "閲� 缃�" [ref=e749] [cursor=pointer]
+ - button "鏌� 璇�" [ref=e751] [cursor=pointer]
+ - generic [ref=e753] [cursor=pointer]: 鏀惰捣
+ - generic [ref=e758]:
+ - tablist [ref=e759]:
+ - generic [ref=e761]:
+ - tab "鍚堝悓閲戦" [selected] [ref=e763] [cursor=pointer]
+ - tab "鍥炴閲戦" [ref=e765] [cursor=pointer]
+ - tab "鍟嗘満閲戦" [ref=e767] [cursor=pointer]
+ - tab "鏂板瀹㈡埛" [ref=e769] [cursor=pointer]
+ - tab "鏂板鍚堝悓" [ref=e771] [cursor=pointer]
+ - generic:
+ - generic:
+ - tabpanel "鍚堝悓閲戦"
+ - generic [ref=e784]:
+ - generic [ref=e785]:
+ - table [ref=e789]:
+ - rowgroup [ref=e809]:
+ - row [ref=e810]:
+ - columnheader "鐩爣瀵硅薄" [ref=e811]
+ - columnheader "鍚堝悓閲戦鐩爣" [ref=e816]
+ - columnheader "瀹為檯鍚堝悓閲戦" [ref=e821]
+ - columnheader "鍚堝悓杈炬垚鐜�(%)" [ref=e826]
+ - columnheader "鍥炴閲戦鐩爣" [ref=e831]
+ - columnheader "瀹為檯鍥炴閲戦" [ref=e836]
+ - columnheader "鍥炴杈炬垚鐜�(%)" [ref=e841]
+ - columnheader "鍟嗘満閲戦鐩爣" [ref=e846]
+ - columnheader "瀹為檯鍟嗘満閲戦" [ref=e851]
+ - columnheader "鍟嗘満杈炬垚鐜�(%)" [ref=e856]
+ - columnheader "鏂板瀹㈡埛鐩爣" [ref=e861]
+ - columnheader "瀹為檯鏂板瀹㈡埛" [ref=e866]
+ - columnheader "瀹㈡埛杈炬垚鐜�(%)" [ref=e871]
+ - columnheader "鏂板鍚堝悓鐩爣" [ref=e876]
+ - columnheader "瀹為檯鏂板鍚堝悓" [ref=e881]
+ - columnheader "鍚堝悓鏁拌揪鎴愮巼(%)" [ref=e886]
+ - table [ref=e894]:
+ - rowgroup [ref=e914]:
+ - row [ref=e915]:
+ - cell "瓒呯骇绠$悊鍛�" [ref=e916]
+ - cell "10000.00" [ref=e919]
+ - cell "32377.00" [ref=e923]
+ - cell "323.77%" [ref=e927]
+ - cell "10000.00" [ref=e930]
+ - cell "100.00" [ref=e934]
+ - cell "1%" [ref=e938]
+ - cell "10000.00" [ref=e941]
+ - cell "0.00" [ref=e945]
+ - cell "0%" [ref=e949]
+ - cell "10000" [ref=e952]
+ - cell "3" [ref=e956]
+ - cell "0.03%" [ref=e960]
+ - cell "10000" [ref=e963]
+ - cell "5" [ref=e967]
+ - cell "0.05%" [ref=e971]
+ - row [ref=e974]:
+ - cell "瓒呯骇绠$悊鍛�" [ref=e975]
+ - cell "500000.00" [ref=e978]
+ - cell "32377.00" [ref=e982]
+ - cell "6.48%" [ref=e986]
+ - cell "300000.00" [ref=e989]
+ - cell "100.00" [ref=e993]
+ - cell "0.03%" [ref=e997]
+ - cell "600000.00" [ref=e1000]
+ - cell "0.00" [ref=e1004]
+ - cell "0%" [ref=e1008]
+ - cell "20" [ref=e1011]
+ - cell "3" [ref=e1015]
+ - cell "15%" [ref=e1019]
+ - cell "30" [ref=e1022]
+ - cell "5" [ref=e1026]
+ - cell "16.67%" [ref=e1030]
+ - generic:
+ - generic [ref=e1033]:
+ - table [ref=e1036]:
+ - rowgroup [ref=e1056]:
+ - row [ref=e1057]:
+ - columnheader "鐩爣鍚嶇О" [ref=e1058]
+ - table [ref=e1065]:
+ - rowgroup [ref=e1068]:
+ - row [ref=e1069]:
+ - cell "閰掓硥" [ref=e1070]
+ - row [ref=e1074]:
+ - cell "2026骞村害閿�鍞洰鏍�-寮犱笁" [ref=e1075]
+ - generic [ref=e1079]:
+ - table [ref=e1082]:
+ - rowgroup [ref=e1102]:
+ - row [ref=e1103]:
+ - columnheader "鐘舵��" [ref=e1104]
+ - table [ref=e1111]:
+ - rowgroup [ref=e1114]:
+ - row [ref=e1115]:
+ - cell "宸茬敓鏁�" [ref=e1116]
+ - row [ref=e1120]:
+ - cell "宸茬敓鏁�" [ref=e1121]
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-08-12T08-16-45-289Z.yml b/.playwright-mcp/page-2026-08-12T08-16-45-289Z.yml
new file mode 100644
index 0000000..4b28c4d
--- /dev/null
+++ b/.playwright-mcp/page-2026-08-12T08-16-45-289Z.yml
@@ -0,0 +1,215 @@
+- generic [ref=e7]:
+ - complementary [ref=e8]:
+ - generic [ref=e9]:
+ - generic [ref=e10] [cursor=pointer]
+ - link "宸ヤ笟AI鏅洪�犲钩鍙� 宸ヤ笟AI鏅洪�犲钩鍙�" [ref=e15] [cursor=pointer]:
+ - /url: javascript:void 0
+ - img "宸ヤ笟AI鏅洪�犲钩鍙�" [ref=e18]
+ - generic [ref=e19]: 宸ヤ笟AI鏅洪�犲钩鍙�
+ - menu [ref=e22]:
+ - menuitem "棣栭〉" [ref=e23] [cursor=pointer]
+ - listitem [ref=e30]:
+ - generic [ref=e31] [cursor=pointer]: 鍩虹鏁版嵁
+ - listitem [ref=e39]:
+ - generic [ref=e40] [cursor=pointer]: 閲囪喘绠$悊
+ - listitem [ref=e46]:
+ - generic [ref=e47] [cursor=pointer]: 钀ラ攢绠$悊
+ - list [ref=e577]:
+ - menuitem "寰呭姙浜嬮」" [ref=e578] [cursor=pointer]
+ - menuitem "绾跨储绠$悊" [ref=e583] [cursor=pointer]
+ - menuitem "鍟嗘満绠$悊" [ref=e588] [cursor=pointer]
+ - menuitem "閿�鍞姤浠�" [ref=e593] [cursor=pointer]
+ - menuitem "鍚堝悓绠$悊" [ref=e599] [cursor=pointer]
+ - menuitem "閿�鍞鍗�" [ref=e605] [cursor=pointer]
+ - menuitem "瀹㈡埛绠$悊" [ref=e610] [cursor=pointer]
+ - menuitem "鑱旂郴浜虹鐞�" [ref=e615] [cursor=pointer]
+ - menuitem "瀹㈡埛鍏捣" [ref=e620] [cursor=pointer]
+ - menuitem "寮�绁ㄧ鐞�" [ref=e625] [cursor=pointer]
+ - menuitem "鍥炴绠$悊" [ref=e630] [cursor=pointer]
+ - menuitem "鍥炴璁″垝" [ref=e637] [cursor=pointer]
+ - menuitem "閿�鍞鍒�" [ref=e642] [cursor=pointer]
+ - menuitem "杈炬垚鍒嗘瀽" [ref=e647] [cursor=pointer]
+ - listitem [ref=e653]:
+ - generic [ref=e654] [cursor=pointer]: 鏁版嵁缁熻
+ - listitem [ref=e660]:
+ - generic [ref=e661] [cursor=pointer]: 绯荤粺閰嶇疆
+ - listitem [ref=e53]:
+ - generic [ref=e54] [cursor=pointer]: 璁惧绠$悊
+ - listitem [ref=e61]:
+ - generic [ref=e62] [cursor=pointer]: 浠撳偍鐗╂祦
+ - listitem [ref=e70]:
+ - generic [ref=e71] [cursor=pointer]: 鐢熶骇绠℃帶
+ - listitem [ref=e78]:
+ - generic [ref=e79] [cursor=pointer]: 璐ㄩ噺绠$悊
+ - listitem [ref=e85]:
+ - generic [ref=e86] [cursor=pointer]: 鍐崇瓥BI澶у睆
+ - listitem [ref=e92]:
+ - generic [ref=e93] [cursor=pointer]: 鍗忓悓鍔炲叕
+ - listitem [ref=e99]:
+ - generic [ref=e100] [cursor=pointer]: 浜哄姏璧勬簮
+ - listitem [ref=e106]:
+ - generic [ref=e107] [cursor=pointer]: 鍞悗绠$悊
+ - listitem [ref=e113]:
+ - generic [ref=e114] [cursor=pointer]: 鍩虹璁炬柦
+ - listitem [ref=e120]:
+ - generic [ref=e121] [cursor=pointer]: 绯荤粺绠$悊
+ - listitem [ref=e127]:
+ - generic [ref=e128] [cursor=pointer]: IM 鍗虫椂閫氳
+ - generic [ref=e136] [cursor=pointer]
+ - generic [ref=e141]:
+ - generic [ref=e142]:
+ - banner [ref=e143]:
+ - button [ref=e144] [cursor=pointer]
+ - button [ref=e147] [cursor=pointer]
+ - navigation "breadcrumb" [ref=e152]:
+ - list [ref=e153]:
+ - listitem [ref=e668]:
+ - link "钀ラ攢绠$悊" [ref=e669] [cursor=pointer]:
+ - /url: javascript:void 0
+ - listitem [ref=e673]
+ - listitem [ref=e676]:
+ - link "杈炬垚鍒嗘瀽" [disabled] [ref=e677]
+ - generic [ref=e162]:
+ - generic [ref=e164] [cursor=pointer]:
+ - generic [ref=e168]: 鎼滅储
+ - generic [ref=e169]: Ctrl K
+ - button [ref=e171] [cursor=pointer]
+ - button "dark" [ref=e176] [cursor=pointer]
+ - button [ref=e185] [cursor=pointer]
+ - button [ref=e191] [cursor=pointer]
+ - button [ref=e194] [cursor=pointer]
+ - button [ref=e200] [cursor=pointer]:
+ - button [ref=e202]
+ - button "宸ヤ綔鍙�" [ref=e206] [cursor=pointer]
+ - button [ref=e210] [cursor=pointer]:
+ - img "瓒呯骇绠$悊鍛�" [ref=e215]
+ - generic [ref=e217]:
+ - generic [ref=e222]:
+ - generic [ref=e223] [cursor=pointer]: 棣栭〉
+ - generic [ref=e684] [cursor=pointer]: 杈炬垚鍒嗘瀽
+ - generic [ref=e239]:
+ - button [ref=e240] [cursor=pointer]
+ - generic [ref=e247] [cursor=pointer]
+ - generic [ref=e251] [cursor=pointer]
+ - main [ref=e258]:
+ - generic [ref=e705]:
+ - generic [ref=e707]:
+ - generic [ref=e708]:
+ - generic [ref=e709]:
+ - generic [ref=e710]: "*"
+ - text: 鍛ㄦ湡绫诲瀷
+ - generic [ref=e716] [cursor=pointer]:
+ - combobox "* 鍛ㄦ湡绫诲瀷" [ref=e718]
+ - generic "骞村害" [ref=e719]
+ - generic [ref=e720]:
+ - generic [ref=e721]:
+ - generic [ref=e722]: "*"
+ - text: 鍛ㄦ湡鍊�
+ - textbox "* 鍛ㄦ湡鍊�" [ref=e727]:
+ - /placeholder: 濡� 2026 / 2026-Q1 / 2026-07
+ - text: "2026"
+ - generic [ref=e728]:
+ - generic [ref=e729]: 鐩爣缁村害
+ - generic [ref=e735] [cursor=pointer]:
+ - combobox "鐩爣缁村害" [ref=e737]
+ - generic: 鍏ㄩ儴
+ - generic [ref=e738]:
+ - generic [ref=e739]: 鐘舵��
+ - generic [ref=e745] [cursor=pointer]:
+ - combobox "鐘舵��" [ref=e747]
+ - generic: 鍏ㄩ儴
+ - generic [ref=e748]:
+ - button "閲� 缃�" [ref=e749] [cursor=pointer]
+ - button "鏌� 璇�" [ref=e751] [cursor=pointer]
+ - generic [ref=e753] [cursor=pointer]: 鏀惰捣
+ - generic [ref=e758]:
+ - tablist [ref=e759]:
+ - generic [ref=e761]:
+ - tab "鍚堝悓閲戦" [ref=e763] [cursor=pointer]
+ - tab "鍥炴閲戦" [active] [selected] [ref=e765] [cursor=pointer]
+ - tab "鍟嗘満閲戦" [ref=e767] [cursor=pointer]
+ - tab "鏂板瀹㈡埛" [ref=e769] [cursor=pointer]
+ - tab "鏂板鍚堝悓" [ref=e771] [cursor=pointer]
+ - generic:
+ - generic:
+ - tabpanel "鍥炴閲戦"
+ - generic [ref=e784]:
+ - generic [ref=e785]:
+ - table [ref=e789]:
+ - rowgroup [ref=e809]:
+ - row [ref=e810]:
+ - columnheader "鐩爣瀵硅薄" [ref=e811]
+ - columnheader "鍚堝悓閲戦鐩爣" [ref=e816]
+ - columnheader "瀹為檯鍚堝悓閲戦" [ref=e821]
+ - columnheader "鍚堝悓杈炬垚鐜�(%)" [ref=e826]
+ - columnheader "鍥炴閲戦鐩爣" [ref=e831]
+ - columnheader "瀹為檯鍥炴閲戦" [ref=e836]
+ - columnheader "鍥炴杈炬垚鐜�(%)" [ref=e841]
+ - columnheader "鍟嗘満閲戦鐩爣" [ref=e846]
+ - columnheader "瀹為檯鍟嗘満閲戦" [ref=e851]
+ - columnheader "鍟嗘満杈炬垚鐜�(%)" [ref=e856]
+ - columnheader "鏂板瀹㈡埛鐩爣" [ref=e861]
+ - columnheader "瀹為檯鏂板瀹㈡埛" [ref=e866]
+ - columnheader "瀹㈡埛杈炬垚鐜�(%)" [ref=e871]
+ - columnheader "鏂板鍚堝悓鐩爣" [ref=e876]
+ - columnheader "瀹為檯鏂板鍚堝悓" [ref=e881]
+ - columnheader "鍚堝悓鏁拌揪鎴愮巼(%)" [ref=e886]
+ - table [ref=e894]:
+ - rowgroup [ref=e914]:
+ - row [ref=e915]:
+ - cell "瓒呯骇绠$悊鍛�" [ref=e916]
+ - cell "10000.00" [ref=e919]
+ - cell "32377.00" [ref=e923]
+ - cell "323.77%" [ref=e927]
+ - cell "10000.00" [ref=e930]
+ - cell "100.00" [ref=e934]
+ - cell "1%" [ref=e938]
+ - cell "10000.00" [ref=e941]
+ - cell "0.00" [ref=e945]
+ - cell "0%" [ref=e949]
+ - cell "10000" [ref=e952]
+ - cell "3" [ref=e956]
+ - cell "0.03%" [ref=e960]
+ - cell "10000" [ref=e963]
+ - cell "5" [ref=e967]
+ - cell "0.05%" [ref=e971]
+ - row [ref=e974]:
+ - cell "瓒呯骇绠$悊鍛�" [ref=e975]
+ - cell "500000.00" [ref=e978]
+ - cell "32377.00" [ref=e982]
+ - cell "6.48%" [ref=e986]
+ - cell "300000.00" [ref=e989]
+ - cell "100.00" [ref=e993]
+ - cell "0.03%" [ref=e997]
+ - cell "600000.00" [ref=e1000]
+ - cell "0.00" [ref=e1004]
+ - cell "0%" [ref=e1008]
+ - cell "20" [ref=e1011]
+ - cell "3" [ref=e1015]
+ - cell "15%" [ref=e1019]
+ - cell "30" [ref=e1022]
+ - cell "5" [ref=e1026]
+ - cell "16.67%" [ref=e1030]
+ - generic:
+ - generic [ref=e1033]:
+ - table [ref=e1036]:
+ - rowgroup [ref=e1056]:
+ - row [ref=e1057]:
+ - columnheader "鐩爣鍚嶇О" [ref=e1058]
+ - table [ref=e1065]:
+ - rowgroup [ref=e1068]:
+ - row [ref=e1069]:
+ - cell "閰掓硥" [ref=e1070]
+ - row [ref=e1074]:
+ - cell "2026骞村害閿�鍞洰鏍�-寮犱笁" [ref=e1075]
+ - generic [ref=e1079]:
+ - table [ref=e1082]:
+ - rowgroup [ref=e1102]:
+ - row [ref=e1103]:
+ - columnheader "鐘舵��" [ref=e1104]
+ - table [ref=e1111]:
+ - rowgroup [ref=e1114]:
+ - row [ref=e1115]:
+ - cell "宸茬敓鏁�" [ref=e1116]
+ - row [ref=e1120]:
+ - cell "宸茬敓鏁�" [ref=e1121]
\ No newline at end of file
diff --git a/docs/crm_sales_target_integration.md b/docs/crm_sales_target_integration.md
new file mode 100644
index 0000000..62036da
--- /dev/null
+++ b/docs/crm_sales_target_integration.md
@@ -0,0 +1,141 @@
+# 钀ラ攢绠$悊 - 閿�鍞鍒掞紙閿�鍞洰鏍囷級鍓嶇鑱旇皟鏂规
+
+## 娑夊強椤甸潰
+
+- 钀ラ攢绠$悊 鈫� 閿�鍞鍒掞紙鑿滃崟 1065000锛岀粍浠� `crm/salesTarget/index`锛�
+- 钀ラ攢绠$悊 鈫� 杈炬垚鍒嗘瀽锛堣彍鍗� 1065100锛岀粍浠� `crm/salesTarget/analysis/index`锛�
+
+## 涓氬姟娴佺▼涓庢暟鎹甫鍏�
+
+1. **璁惧畾鐩爣**锛氱鐞嗗憳鍦ㄣ�岄攢鍞鍒掋�嶉〉鎸夊懆鏈燂紙骞�/瀛�/鏈堬級+ 缁村害锛堜釜浜�/閮ㄩ棬/浜у搧锛夎瀹氶攢鍞洰鏍囥�傜洰鏍囧璞★紙鐢ㄦ埛/閮ㄩ棬/鐗╂枡锛変粠绯荤粺涓嬫媺閫夋嫨锛岀洰鏍囨寚鏍囷紙鍚堝悓閲戦銆佸洖娆鹃噾棰濄�佸晢鏈洪噾棰濄�佹柊澧炲鎴锋暟銆佹柊澧炲悎鍚屾暟锛夋墜宸ュ綍鍏ャ��
+2. **鍛ㄦ湡鑷姩鎹㈢畻**锛氬墠绔彧鎻愪氦 `periodType`锛�1骞�/2瀛e害/3鏈堬級涓� `periodValue`锛堝 `2026`銆乣2026-Q1`銆乣2026-07`锛夛紝鍚庡彴鑷姩鎹㈢畻鍛ㄦ湡璧锋鏃堕棿骞惰惤搴擄紱鍒楄〃/璇︽儏灞曠ず鎹㈢畻鍚庣殑璧锋鏃堕棿銆�
+3. **鐘舵�佽嚜鍔ㄥ垽瀹�**锛氬悗鍙颁繚瀛樻椂鎸夊綋鍓嶆椂闂翠笌鍛ㄦ湡鑷姩鍒ゅ畾鐘舵�侊紙鏈敓鏁�/宸茬敓鏁�/宸茬粨鏉燂級锛屽墠绔睍绀哄嵆鍙紱浠呫�屼綔搴�/鎭㈠銆嶇敱鐢ㄦ埛鎵嬪姩瑙﹀彂銆�
+4. **杈炬垚缁熻鑷姩甯﹀叆**锛氬垪琛ㄣ�佽鎯呫�佽揪鎴愬垎鏋愬潎杩斿洖瀹為檯杈炬垚锛堝悎鍚岄噾棰濄�佸洖娆鹃噾棰濄�佸晢鏈洪噾棰濄�佹柊澧炲鎴锋暟銆佹柊澧炲悎鍚屾暟锛変笌杈炬垚鐜囷紝鍓嶇鐩存帴灞曠ず锛屾棤闇�鍙﹁璋冪敤缁熻鎺ュ彛銆�
+5. **杈炬垚鍒嗘瀽**锛氱敤鎴峰湪銆岃揪鎴愬垎鏋愩�嶉〉閫夋嫨鍛ㄦ湡锛堝繀閫夛級+ 缁村害锛堝彲閫夛級+ 鐘舵�侊紙鍙�夛級锛屽悗鍙版寜鍛ㄦ湡杩囨护杩斿洖鐩爣鍒楄〃锛堝惈瀹為檯杈炬垚涓庤揪鎴愮巼锛夈��
+
+## API
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| POST | `/crm/sales-target/create` | 鍒涘缓閿�鍞洰鏍� |
+| PUT | `/crm/sales-target/update` | 鏇存柊閿�鍞洰鏍� |
+| PUT | `/crm/sales-target/change-status` | 鍙樻洿鐩爣鐘舵�侊紙浣滃簾/鎭㈠/鐢熸晥锛� |
+| DELETE | `/crm/sales-target/delete?id=` | 鍒犻櫎閿�鍞洰鏍� |
+| GET | `/crm/sales-target/get?id=` | 鐩爣璇︽儏锛堝惈瀹為檯杈炬垚+杈炬垚鐜囷級 |
+| GET | `/crm/sales-target/page` | 鐩爣鍒嗛〉锛堝惈瀹為檯杈炬垚+杈炬垚鐜囷級 |
+| GET | `/crm/sales-target/export-excel` | 瀵煎嚭 Excel |
+| GET | `/crm/sales-target/get-achievement-analysis` | 杈炬垚鍒嗘瀽 |
+
+### 鍒涘缓/鏇存柊璇锋眰鍙傛暟锛圕rmSalesTargetSaveReqVO锛�
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏇存柊鏃跺繀濉� | 鐩爣缂栧彿 |
+| name | String | 鏄� | 鐩爣鍚嶇О |
+| periodType | Integer | 鏄� | 鍛ㄦ湡绫诲瀷锛堝瓧鍏� crm_sales_target_period_type锛�1骞�/2瀛e害/3鏈堬級 |
+| periodValue | String | 鏄� | 鍛ㄦ湡鍊硷紝濡� `2026` / `2026-Q1` / `2026-07` |
+| targetDimension | Integer | 鏄� | 鐩爣缁村害锛堝瓧鍏� crm_sales_target_dimension锛�1涓汉/2閮ㄩ棬/3浜у搧锛� |
+| targetUserId | Long | 缁村害=涓汉鏃跺繀濉� | 鐩爣鐢ㄦ埛缂栧彿 |
+| targetDeptId | Long | 缁村害=閮ㄩ棬鏃跺繀濉� | 鐩爣閮ㄩ棬缂栧彿 |
+| targetItemId | Long | 缁村害=浜у搧鏃跺繀濉� | 鐩爣鐗╂枡缂栧彿锛堝叧鑱� MDM锛� |
+| contractTarget | BigDecimal | 鍚� | 鍚堝悓閲戦鐩爣锛堝厓锛� |
+| receivableTarget | BigDecimal | 鍚� | 鍥炴閲戦鐩爣锛堝厓锛� |
+| businessTarget | BigDecimal | 鍚� | 鍟嗘満閲戦鐩爣锛堝厓锛� |
+| customerTarget | Integer | 鍚� | 鏂板瀹㈡埛鏁扮洰鏍� |
+| contractCountTarget | Integer | 鍚� | 鏂板鍚堝悓鏁扮洰鏍� |
+| remark | String | 鍚� | 澶囨敞 |
+
+### 鐘舵�佸彉鏇磋姹傚弬鏁帮紙CrmSalesTargetChangeStatusReqVO锛�
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏄� | 鐩爣缂栧彿 |
+| status | Integer | 鏄� | 鐩爣鐘舵�侊紙瀛楀吀 crm_sales_target_status锛�0鏈敓鏁�/1宸茬敓鏁�/2宸茬粨鏉�/3宸蹭綔搴燂級 |
+
+### 杈炬垚鍒嗘瀽璇锋眰鍙傛暟锛圕rmSalesTargetAnalysisReqVO锛�
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| periodType | Integer | 鏄� | 鍛ㄦ湡绫诲瀷 |
+| periodValue | String | 鏄� | 鍛ㄦ湡鍊� |
+| targetDimension | Integer | 鍚� | 鐩爣缁村害锛屼负绌烘煡鍏ㄩ儴 |
+| status | Integer | 鍚� | 鐘舵�侊紝涓虹┖鏌ュ叏閮� |
+
+### 鍝嶅簲瀛楁锛圕rmSalesTargetRespVO锛�
+
+鐩爣鍩虹瀛楁锛坣ame/periodType/periodValue/startTime/endTime/targetDimension/status/remark/createTime/creator锛変箣涓婏紝鏂板浠ヤ笅瀛楁锛�
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| targetUserName | String | 鐩爣瀵硅薄鍚嶇О锛堢淮搴�=涓汉鏃朵负鏄电О锛� |
+| targetDeptName | String | 鐩爣閮ㄩ棬鍚嶇О锛堢淮搴�=閮ㄩ棬锛� |
+| targetItemName | String | 鐩爣鐗╂枡鍚嶇О锛堢淮搴�=浜у搧锛� |
+| actualContractPrice | BigDecimal | 瀹為檯鍚堝悓閲戦锛堝厓锛� |
+| actualReceivablePrice | BigDecimal | 瀹為檯鍥炴閲戦锛堝厓锛� |
+| actualBusinessPrice | BigDecimal | 瀹為檯鍟嗘満閲戦锛堝厓锛� |
+| actualCustomerCount | Long | 瀹為檯鏂板瀹㈡埛鏁� |
+| actualContractCount | Long | 瀹為檯鏂板鍚堝悓鏁� |
+| contractRate | BigDecimal | 鍚堝悓閲戦杈炬垚鐜囷紙%锛� |
+| receivableRate | BigDecimal | 鍥炴閲戦杈炬垚鐜囷紙%锛� |
+| businessRate | BigDecimal | 鍟嗘満閲戦杈炬垚鐜囷紙%锛� |
+| customerRate | BigDecimal | 鏂板瀹㈡埛鏁拌揪鎴愮巼锛�%锛� |
+| contractCountRate | BigDecimal | 鏂板鍚堝悓鏁拌揪鎴愮巼锛�%锛� |
+| creatorName | String | 鍒涘缓浜哄悕绉� |
+
+**鍝嶅簲绀轰緥锛�**
+
+```json
+{
+ "code": 0,
+ "data": {
+ "id": 1,
+ "name": "2026骞村害閿�鍞洰鏍�-寮犱笁",
+ "periodType": 1,
+ "periodValue": "2026",
+ "startTime": "2026-01-01 00:00:00",
+ "endTime": "2027-01-01 00:00:00",
+ "targetDimension": 1,
+ "targetUserId": 1,
+ "targetUserName": "寮犱笁",
+ "contractTarget": 500000.00,
+ "actualContractPrice": 32377.00,
+ "contractRate": 6.48,
+ "status": 1
+ }
+}
+```
+
+## 瀛楁灞曠ず瑙勫垯
+
+| 瀛楁 | 灞曠ず浣嶇疆 | 璇存槑 |
+|------|----------|------|
+| 鐩爣鍚嶇О | 鍒楄〃銆佽鎯� | 涓绘爣棰� |
+| 鍛ㄦ湡 | 鍒楄〃銆佽鎯� | 鍛ㄦ湡绫诲瀷瀛楀吀杞枃鏈� + 鍛ㄦ湡鍊硷紝濡傘�屽勾搴� 2026銆� |
+| 鐩爣瀵硅薄 | 鍒楄〃銆佽鎯� | 鎸夌淮搴﹀睍绀虹洰鏍囩敤鎴�/閮ㄩ棬/鐗╂枡鍚嶇О锛坱argetUserName/targetDeptName/targetItemName锛� |
+| 鐩爣鎸囨爣 | 鍒楄〃銆佽鎯呫�佸垎鏋� | 鍚堝悓閲戦/鍥炴閲戦/鍟嗘満閲戦/鏂板瀹㈡埛鏁�/鏂板鍚堝悓鏁扮洰鏍� |
+| 瀹為檯杈炬垚 | 鍒楄〃銆佽鎯呫�佸垎鏋� | 涓庣洰鏍囨寚鏍囦竴涓�瀵瑰簲鐨勫疄闄呭�� |
+| 杈炬垚鐜� | 鍒楄〃銆佽鎯呫�佸垎鏋� | 鐧惧垎姣旓紝淇濈暀涓や綅灏忔暟锛涚洰鏍囦负 0 鎴栫┖鏃惰揪鎴愮巼涓虹┖锛堝睍绀轰负 `-`锛� |
+| 鐘舵�� | 鍒楄〃銆佽鎯� | 瀛楀吀杞枃鏈紝宸蹭綔搴熼渶缃伆/绂佹缂栬緫 |
+| 鍒涘缓浜� | 鍒楄〃銆佽鎯� | 鍒涘缓浜烘樀绉� |
+
+## 涓氬姟瑙勫垯璇存槑
+
+| 鍦烘櫙 | 瑙勫垯 |
+|------|------|
+| 杈炬垚鐜囪绠� | 瀹為檯鍊� 梅 鐩爣鍊� 脳 100锛屼繚鐣� 2 浣嶅皬鏁帮紱鐩爣涓虹┖鎴� 鈮�0 鏃惰揪鎴愮巼杩斿洖 null |
+| 鍛ㄦ湡鎹㈢畻 | 鍓嶇鏃犻渶璁$畻璧锋鏃堕棿锛屽悗鍙版寜 periodValue 鎹㈢畻锛涘勾 `2026`銆佸 `2026-Q1`銆佹湀 `2026-07` |
+| 鐘舵�佸垽瀹� | 鍚庡彴鎸夊綋鍓嶆椂闂磋嚜鍔ㄥ垽瀹� 鏈敓鏁�/宸茬敓鏁�/宸茬粨鏉燂紱鍓嶇鏂板缂栬緫鏃舵棤闇�鎵嬪姩閫夌姸鎬� |
+| 浣滃簾/鎭㈠ | 宸蹭綔搴熺殑鐩爣琚紪杈戝悗浠嶄繚鎸佷綔搴熺姸鎬侊紝闇�鍏堛�屾仮澶嶃�嶅啀缂栬緫鐢熸晥 |
+| 缁熻鍙e緞 | 鍚堝悓閲戦/鏁伴噺鍙粺璁″鏍搁�氳繃锛坅udit_status=20锛夌殑鍚堝悓锛涘洖娆惧彧缁熻瀹℃牳閫氳繃鐨勫洖娆撅紱鍟嗘満鍙粺璁¤耽鍗曪紙end_status=1锛夛紱瀹㈡埛鎸夊垱寤烘椂闂寸粺璁� |
+| 浜у搧缁村害 | 浠呯粺璁″悎鍚岄噾棰濅笌鍚堝悓鏁帮紝鍥炴/鍟嗘満/瀹㈡埛鏁颁负 0 |
+| 閮ㄩ棬缁村害 | 鍚瓙閮ㄩ棬涓嬫墍鏈夌敤鎴风殑鏁版嵁 |
+| 鏁版嵁鏉冮檺 | 鐩爣涓虹鐞嗛厤缃紝涓嶆帴鍏� CRM 鏁版嵁鏉冮檺锛屽垪琛ㄥ睍绀哄叏閮紙鍙寜闇�鎸夊垱寤轰汉杩囨护锛� |
+
+## 娉ㄦ剰浜嬮」
+
+- 杈炬垚鐜囧瓧娈靛彲鑳戒负 null锛屽墠绔睍绀洪渶鍏滃簳涓� `-`锛岄伩鍏嶆樉绀� 0銆�
+- 鍛ㄦ湡鍊兼牸寮忎弗鏍硷細骞� `YYYY`銆佸 `YYYY-Q1`~`YYYY-Q4`銆佹湀 `YYYY-MM`锛屽墠绔〃鍗曟牎楠岄渶鎸夋绾︽潫銆�
+- 鐩爣缁村害涓庣洰鏍囧璞′簰鏂ワ細缁村害=涓汉濉� userId锛岄儴闂ㄥ~ deptId锛屼骇鍝佸~ itemId锛屽叾浣欑暀绌猴紱鍓嶇鎸夌淮搴﹀垏鎹㈣〃鍗曢」銆�
+- 杈炬垚鍒嗘瀽鎺ュ彛鐨� periodType + periodValue 涓哄繀濉紝鏈�夊懆鏈熸椂涓嶅簲鍙戣捣璇锋眰銆�
+- 杈炬垚缁熻鏌ヨ涓洪�愭潯鐩爣璁$畻锛堢害 5 鏉″皬 SQL锛夛紝鐩爣鏁伴噺绾у嚑鍗亊鍑犵櫨鏃舵�ц兘鍙帴鍙楋紱鑻ュ悗缁洰鏍囬噺绾у澶ч渶鏀归�犱负鑱氬悎缁熻锛岄渶鎻愬墠鐭ヤ細鍚庣銆�
+- 銆岄攢鍞鍒掋�嶉〉鎺掑簭寤鸿鎸夊垱寤烘椂闂村�掑簭锛屼笌鍚庡彴杩斿洖椤哄簭涓�鑷达紙鎸� id 鍊掑簭锛夈��
diff --git a/docs/sql/crm_sales_target_init.sql b/docs/sql/crm_sales_target_init.sql
new file mode 100644
index 0000000..6a90913
--- /dev/null
+++ b/docs/sql/crm_sales_target_init.sql
@@ -0,0 +1,75 @@
+-- =====================================================
+-- CRM 閿�鍞洰鏍囷紙閿�鍞鍒掞級鍔熻兘鍒濆鍖栬剼鏈�
+-- 1. 寤鸿〃 crm_sales_target
+-- 2. 鏁版嵁瀛楀吀锛堝懆鏈熺被鍨� / 鐩爣缁村害 / 鐘舵�侊級
+-- 3. 鑿滃崟涓庢寜閽潈闄愶紙钀ラ攢绠$悊 -> 閿�鍞鍒� / 杈炬垚鍒嗘瀽锛�
+-- =====================================================
+
+-- 1. 寤鸿〃
+CREATE TABLE IF NOT EXISTS `crm_sales_target` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
+ `name` varchar(128) NOT NULL COMMENT '鐩爣鍚嶇О',
+ `period_type` int NOT NULL COMMENT '鍛ㄦ湡绫诲瀷(瀛楀吀 crm_sales_target_period_type: 1骞�/2瀛e害/3鏈�)',
+ `period_value` varchar(32) NOT NULL COMMENT '鍛ㄦ湡鍊硷紝濡� 2026 / 2026-Q1 / 2026-07',
+ `start_time` datetime NOT NULL COMMENT '鍛ㄦ湡寮�濮嬫椂闂�(淇濆瓨鏃剁敱鍛ㄦ湡璁$畻鍐欏叆)',
+ `end_time` datetime NOT NULL COMMENT '鍛ㄦ湡缁撴潫鏃堕棿(淇濆瓨鏃剁敱鍛ㄦ湡璁$畻鍐欏叆)',
+ `target_dimension` int NOT NULL COMMENT '鐩爣缁村害(瀛楀吀 crm_sales_target_dimension: 1涓汉/2閮ㄩ棬/3浜у搧)',
+ `target_user_id` bigint DEFAULT NULL COMMENT '鐩爣鐢ㄦ埛缂栧彿(缁村害=涓汉)',
+ `target_dept_id` bigint DEFAULT NULL COMMENT '鐩爣閮ㄩ棬缂栧彿(缁村害=閮ㄩ棬)',
+ `target_item_id` bigint DEFAULT NULL COMMENT '鐩爣鐗╂枡缂栧彿(缁村害=浜у搧锛屽叧鑱� MDM)',
+ `contract_target` decimal(18,2) DEFAULT NULL COMMENT '鍚堝悓閲戦鐩爣(鍏�)',
+ `receivable_target` decimal(18,2) DEFAULT NULL COMMENT '鍥炴閲戦鐩爣(鍏�)',
+ `business_target` decimal(18,2) DEFAULT NULL COMMENT '鍟嗘満閲戦鐩爣(鍏�)',
+ `customer_target` int DEFAULT NULL COMMENT '鏂板瀹㈡埛鏁扮洰鏍�',
+ `contract_count_target` int DEFAULT NULL COMMENT '鏂板鍚堝悓鏁扮洰鏍�',
+ `status` int NOT NULL DEFAULT 0 COMMENT '鐘舵��(瀛楀吀 crm_sales_target_status: 0鏈敓鏁�/1宸茬敓鏁�/2宸茬粨鏉�/3宸蹭綔搴�)',
+ `remark` varchar(255) DEFAULT NULL COMMENT '澶囨敞',
+ `creator` varchar(64) DEFAULT NULL COMMENT '鍒涘缓鑰�',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `updater` varchar(64) DEFAULT NULL COMMENT '鏇存柊鑰�',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏈�鍚庢洿鏂版椂闂�',
+ `deleted` tinyint NOT NULL DEFAULT 0 COMMENT '鏄惁鍒犻櫎',
+ PRIMARY KEY (`id`),
+ KEY `idx_period` (`period_type`,`period_value`),
+ KEY `idx_dimension` (`target_dimension`,`target_user_id`,`target_dept_id`,`target_item_id`),
+ KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM 閿�鍞洰鏍�';
+
+-- 2. 鏁版嵁瀛楀吀绫诲瀷
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`)
+VALUES (1065300, '閿�鍞洰鏍囧懆鏈熺被鍨�', 'crm_sales_target_period_type', 0, '閿�鍞洰鏍囩殑璁″垝鍛ㄦ湡绫诲瀷', '1', NOW(), '1', NOW(), b'0', NULL),
+ (1065301, '閿�鍞洰鏍囩淮搴�', 'crm_sales_target_dimension', 0, '閿�鍞洰鏍囨寜浠�涔堢淮搴﹁瀹�', '1', NOW(), '1', NOW(), b'0', NULL),
+ (1065302, '閿�鍞洰鏍囩姸鎬�', 'crm_sales_target_status', 0, '閿�鍞洰鏍囩殑鐢熷懡鍛ㄦ湡鐘舵��', '1', NOW(), '1', NOW(), b'0', NULL);
+
+-- 2.1 鏁版嵁瀛楀吀鏁版嵁
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+VALUES
+ -- 鍛ㄦ湡绫诲瀷
+ (1065310, 1, '骞村害', '1', 'crm_sales_target_period_type', 0, 'primary', '', '', '1', NOW(), '1', NOW(), b'0'),
+ (1065311, 2, '瀛e害', '2', 'crm_sales_target_period_type', 0, 'success', '', '', '1', NOW(), '1', NOW(), b'0'),
+ (1065312, 3, '鏈堝害', '3', 'crm_sales_target_period_type', 0, 'warning', '', '', '1', NOW(), '1', NOW(), b'0'),
+ -- 鐩爣缁村害
+ (1065320, 1, '涓汉', '1', 'crm_sales_target_dimension', 0, 'primary', '', '', '1', NOW(), '1', NOW(), b'0'),
+ (1065321, 2, '閮ㄩ棬', '2', 'crm_sales_target_dimension', 0, 'success', '', '', '1', NOW(), '1', NOW(), b'0'),
+ (1065322, 3, '浜у搧', '3', 'crm_sales_target_dimension', 0, 'warning', '', '', '1', NOW(), '1', NOW(), b'0'),
+ -- 鐘舵��
+ (1065330, 0, '鏈敓鏁�', '0', 'crm_sales_target_status', 0, 'info', '', '', '1', NOW(), '1', NOW(), b'0'),
+ (1065331, 1, '宸茬敓鏁�', '1', 'crm_sales_target_status', 0, 'success', '', '', '1', NOW(), '1', NOW(), b'0'),
+ (1065332, 2, '宸茬粨鏉�', '2', 'crm_sales_target_status', 0, 'warning', '', '', '1', NOW(), '1', NOW(), b'0'),
+ (1065333, 3, '宸蹭綔搴�', '3', 'crm_sales_target_status', 0, 'danger', '', '', '1', NOW(), '1', NOW(), b'0');
+
+-- 3. 鑿滃崟锛氳惀閿�绠$悊(id=2617) -> 閿�鍞鍒� / 杈炬垚鍒嗘瀽
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+VALUES
+ (1065000, '閿�鍞鍒�', '', 2, 62, 2617, 'salesTarget', 'ep:data-line', 'crm/salesTarget/index', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+ (1065100, '杈炬垚鍒嗘瀽', '', 2, 63, 2617, 'salesTarget/analysis', 'ep:pie-chart', 'crm/salesTarget/analysis/index', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
+
+-- 3.1 閿�鍞鍒掓寜閽潈闄�
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+VALUES
+ (1065001, '閿�鍞鍒掓煡璇�', 'crm:sales-target:query', 3, 1, 1065000, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+ (1065002, '閿�鍞鍒掑垱寤�', 'crm:sales-target:create', 3, 2, 1065000, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+ (1065003, '閿�鍞鍒掓洿鏂�', 'crm:sales-target:update', 3, 3, 1065000, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+ (1065004, '閿�鍞鍒掑垹闄�', 'crm:sales-target:delete', 3, 4, 1065000, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+ (1065005, '閿�鍞鍒掑鍑�', 'crm:sales-target:export', 3, 5, 1065000, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+ (1065101, '杈炬垚鍒嗘瀽鏌ヨ', 'crm:sales-target:query', 3, 1, 1065100, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
diff --git a/sales-target-analysis.png b/sales-target-analysis.png
new file mode 100644
index 0000000..41c6439
--- /dev/null
+++ b/sales-target-analysis.png
Binary files differ
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/CrmSalesTargetController.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/CrmSalesTargetController.java
new file mode 100644
index 0000000..34d1ec7
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/CrmSalesTargetController.java
@@ -0,0 +1,104 @@
+package cn.iocoder.yudao.module.crm.controller.admin.salesTarget;
+
+import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.CrmSalesTargetChangeStatusReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.CrmSalesTargetPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.CrmSalesTargetRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.CrmSalesTargetSaveReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.analysis.CrmSalesTargetAnalysisReqVO;
+import cn.iocoder.yudao.module.crm.service.salesTarget.CrmSalesTargetService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
+
+@Tag(name = "绠$悊鍚庡彴 - CRM 閿�鍞洰鏍�")
+@RestController
+@RequestMapping("/crm/sales-target")
+@Validated
+public class CrmSalesTargetController {
+
+ @Resource
+ private CrmSalesTargetService salesTargetService;
+
+ @PostMapping("/create")
+ @Operation(summary = "鍒涘缓閿�鍞洰鏍�")
+ @PreAuthorize("@ss.hasPermission('crm:sales-target:create')")
+ public CommonResult<Long> createSalesTarget(@Valid @RequestBody CrmSalesTargetSaveReqVO createReqVO) {
+ return success(salesTargetService.createSalesTarget(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "鏇存柊閿�鍞洰鏍�")
+ @PreAuthorize("@ss.hasPermission('crm:sales-target:update')")
+ public CommonResult<Boolean> updateSalesTarget(@Valid @RequestBody CrmSalesTargetSaveReqVO updateReqVO) {
+ salesTargetService.updateSalesTarget(updateReqVO);
+ return success(true);
+ }
+
+ @PutMapping("/change-status")
+ @Operation(summary = "鍙樻洿閿�鍞洰鏍囩姸鎬�", description = "浣滃簾/鎭㈠/鐢熸晥绛�")
+ @PreAuthorize("@ss.hasPermission('crm:sales-target:update')")
+ public CommonResult<Boolean> changeSalesTargetStatus(@Valid @RequestBody CrmSalesTargetChangeStatusReqVO reqVO) {
+ salesTargetService.changeSalesTargetStatus(reqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "鍒犻櫎閿�鍞洰鏍�")
+ @Parameter(name = "id", description = "缂栧彿", required = true)
+ @PreAuthorize("@ss.hasPermission('crm:sales-target:delete')")
+ public CommonResult<Boolean> deleteSalesTarget(@RequestParam("id") Long id) {
+ salesTargetService.deleteSalesTarget(id);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "鑾峰緱閿�鍞洰鏍�", description = "鍚疄闄呰揪鎴愪笌杈炬垚鐜�")
+ @Parameter(name = "id", description = "缂栧彿", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('crm:sales-target:query')")
+ public CommonResult<CrmSalesTargetRespVO> getSalesTarget(@RequestParam("id") Long id) {
+ return success(salesTargetService.getSalesTarget(id));
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "鑾峰緱閿�鍞洰鏍囧垎椤�", description = "鍚疄闄呰揪鎴愪笌杈炬垚鐜�")
+ @PreAuthorize("@ss.hasPermission('crm:sales-target:query')")
+ public CommonResult<PageResult<CrmSalesTargetRespVO>> getSalesTargetPage(@Valid CrmSalesTargetPageReqVO pageReqVO) {
+ return success(salesTargetService.getSalesTargetPage(pageReqVO));
+ }
+
+ @GetMapping("/export-excel")
+ @Operation(summary = "瀵煎嚭閿�鍞洰鏍� Excel")
+ @PreAuthorize("@ss.hasPermission('crm:sales-target:export')")
+ @ApiAccessLog(operateType = EXPORT)
+ public void exportSalesTargetExcel(@Valid CrmSalesTargetPageReqVO exportReqVO,
+ HttpServletResponse response) throws IOException {
+ exportReqVO.setPageSize(PAGE_SIZE_NONE);
+ List<CrmSalesTargetRespVO> list = salesTargetService.getSalesTargetList(exportReqVO);
+ ExcelUtils.write(response, "閿�鍞洰鏍�.xls", "鏁版嵁", CrmSalesTargetRespVO.class, list);
+ }
+
+ @GetMapping("/get-achievement-analysis")
+ @Operation(summary = "鑾峰緱閿�鍞洰鏍囪揪鎴愬垎鏋�", description = "鎸夊懆鏈� + 缁村害杩囨护鐩爣锛岃繑鍥炵洰鏍� vs 瀹為檯 + 杈炬垚鐜�")
+ @PreAuthorize("@ss.hasPermission('crm:sales-target:query')")
+ public CommonResult<List<CrmSalesTargetRespVO>> getSalesTargetAnalysis(@Valid CrmSalesTargetAnalysisReqVO reqVO) {
+ return success(salesTargetService.getSalesTargetAnalysis(reqVO));
+ }
+
+}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/CrmSalesTargetChangeStatusReqVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/CrmSalesTargetChangeStatusReqVO.java
new file mode 100644
index 0000000..6530cf2
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/CrmSalesTargetChangeStatusReqVO.java
@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.crm.enums.salesTarget.CrmSalesTargetStatusEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "绠$悊鍚庡彴 - CRM 閿�鍞洰鏍囩姸鎬佸彉鏇� Request VO")
+@Data
+public class CrmSalesTargetChangeStatusReqVO {
+
+ @Schema(description = "缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "缂栧彿涓嶈兘涓虹┖")
+ private Long id;
+
+ @Schema(description = "鐘舵��", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
+ @NotNull(message = "鐘舵�佷笉鑳戒负绌�")
+ @InEnum(CrmSalesTargetStatusEnum.class)
+ private Integer status;
+
+}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/CrmSalesTargetPageReqVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/CrmSalesTargetPageReqVO.java
new file mode 100644
index 0000000..96bcb58
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/CrmSalesTargetPageReqVO.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "绠$悊鍚庡彴 - CRM 閿�鍞洰鏍囧垎椤� Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmSalesTargetPageReqVO extends PageParam {
+
+ @Schema(description = "鐩爣鍚嶇О", example = "2026 涓婂崐骞撮攢鍞洰鏍�")
+ private String name;
+
+ @Schema(description = "鍛ㄦ湡绫诲瀷", example = "1")
+ private Integer periodType;
+
+ @Schema(description = "鍛ㄦ湡鍊硷紝濡� 2026 / 2026-Q1 / 2026-07", example = "2026-Q1")
+ private String periodValue;
+
+ @Schema(description = "鐩爣缁村害", example = "1")
+ private Integer targetDimension;
+
+ @Schema(description = "鐘舵��", example = "1")
+ private Integer status;
+
+}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/CrmSalesTargetRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/CrmSalesTargetRespVO.java
new file mode 100644
index 0000000..a42f655
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/CrmSalesTargetRespVO.java
@@ -0,0 +1,130 @@
+package cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import cn.iocoder.yudao.module.crm.enums.DictTypeConstants;
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "绠$悊鍚庡彴 - CRM 閿�鍞洰鏍� Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class CrmSalesTargetRespVO {
+
+ @Schema(description = "缂栧彿", example = "1")
+ @ExcelProperty("缂栧彿")
+ private Long id;
+
+ @Schema(description = "鐩爣鍚嶇О", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026 涓婂崐骞撮攢鍞洰鏍�")
+ @ExcelProperty("鐩爣鍚嶇О")
+ private String name;
+
+ @Schema(description = "鍛ㄦ湡绫诲瀷", example = "1")
+ @ExcelProperty(value = "鍛ㄦ湡绫诲瀷", converter = DictConvert.class)
+ @DictFormat(DictTypeConstants.CRM_SALES_TARGET_PERIOD_TYPE)
+ private Integer periodType;
+
+ @Schema(description = "鍛ㄦ湡鍊硷紝濡� 2026 / 2026-Q1 / 2026-07", example = "2026-Q1")
+ @ExcelProperty("鍛ㄦ湡鍊�")
+ private String periodValue;
+
+ @Schema(description = "鍛ㄦ湡寮�濮嬫椂闂�", example = "2026-01-01 00:00:00")
+ private LocalDateTime startTime;
+
+ @Schema(description = "鍛ㄦ湡缁撴潫鏃堕棿", example = "2026-03-31 23:59:59")
+ private LocalDateTime endTime;
+
+ @Schema(description = "鐩爣缁村害", example = "1")
+ @ExcelProperty(value = "鐩爣缁村害", converter = DictConvert.class)
+ @DictFormat(DictTypeConstants.CRM_SALES_TARGET_DIMENSION)
+ private Integer targetDimension;
+
+ @Schema(description = "鐩爣鐢ㄦ埛缂栧彿锛堢淮搴�=涓汉锛�", example = "1")
+ private Long targetUserId;
+ @Schema(description = "鐩爣鐢ㄦ埛鍚嶇О", example = "寮犱笁")
+ @ExcelProperty("鐩爣瀵硅薄")
+ private String targetUserName;
+
+ @Schema(description = "鐩爣閮ㄩ棬缂栧彿锛堢淮搴�=閮ㄩ棬锛�", example = "100")
+ private Long targetDeptId;
+ @Schema(description = "鐩爣閮ㄩ棬鍚嶇О", example = "閿�鍞儴")
+ private String targetDeptName;
+
+ @Schema(description = "鐩爣鐗╂枡缂栧彿锛堢淮搴�=浜у搧锛�", example = "1")
+ private Long targetItemId;
+ @Schema(description = "鐩爣鐗╂枡鍚嶇О", example = "铻轰笣")
+ private String targetItemName;
+
+ @Schema(description = "鍚堝悓閲戦鐩爣锛堝厓锛�", example = "1000000")
+ @ExcelProperty("鍚堝悓閲戦鐩爣")
+ private BigDecimal contractTarget;
+ @Schema(description = "鍥炴閲戦鐩爣锛堝厓锛�", example = "800000")
+ @ExcelProperty("鍥炴閲戦鐩爣")
+ private BigDecimal receivableTarget;
+ @Schema(description = "鍟嗘満閲戦鐩爣锛堝厓锛�", example = "1500000")
+ @ExcelProperty("鍟嗘満閲戦鐩爣")
+ private BigDecimal businessTarget;
+ @Schema(description = "鏂板瀹㈡埛鏁扮洰鏍�", example = "50")
+ @ExcelProperty("鏂板瀹㈡埛鏁扮洰鏍�")
+ private Integer customerTarget;
+ @Schema(description = "鏂板鍚堝悓鏁扮洰鏍�", example = "30")
+ @ExcelProperty("鏂板鍚堝悓鏁扮洰鏍�")
+ private Integer contractCountTarget;
+
+ @Schema(description = "瀹為檯鍚堝悓閲戦锛堝厓锛�", example = "500000")
+ @ExcelProperty("瀹為檯鍚堝悓閲戦")
+ private BigDecimal actualContractPrice;
+ @Schema(description = "瀹為檯鍥炴閲戦锛堝厓锛�", example = "300000")
+ @ExcelProperty("瀹為檯鍥炴閲戦")
+ private BigDecimal actualReceivablePrice;
+ @Schema(description = "瀹為檯鍟嗘満閲戦锛堝厓锛�", example = "800000")
+ @ExcelProperty("瀹為檯鍟嗘満閲戦")
+ private BigDecimal actualBusinessPrice;
+ @Schema(description = "瀹為檯鏂板瀹㈡埛鏁�", example = "20")
+ @ExcelProperty("瀹為檯鏂板瀹㈡埛鏁�")
+ private Long actualCustomerCount;
+ @Schema(description = "瀹為檯鏂板鍚堝悓鏁�", example = "15")
+ @ExcelProperty("瀹為檯鏂板鍚堝悓鏁�")
+ private Long actualContractCount;
+
+ @Schema(description = "鍚堝悓杈炬垚鐜囷紙鐧惧垎姣旓級", example = "50.0")
+ @ExcelProperty("鍚堝悓杈炬垚鐜�(%)")
+ private BigDecimal contractRate;
+ @Schema(description = "鍥炴杈炬垚鐜囷紙鐧惧垎姣旓級", example = "37.5")
+ @ExcelProperty("鍥炴杈炬垚鐜�(%)")
+ private BigDecimal receivableRate;
+ @Schema(description = "鍟嗘満杈炬垚鐜囷紙鐧惧垎姣旓級", example = "53.3")
+ @ExcelProperty("鍟嗘満杈炬垚鐜�(%)")
+ private BigDecimal businessRate;
+ @Schema(description = "鏂板瀹㈡埛鏁拌揪鎴愮巼锛堢櫨鍒嗘瘮锛�", example = "40.0")
+ @ExcelProperty("鏂板瀹㈡埛鏁拌揪鎴愮巼(%)")
+ private BigDecimal customerRate;
+ @Schema(description = "鏂板鍚堝悓鏁拌揪鎴愮巼锛堢櫨鍒嗘瘮锛�", example = "50.0")
+ @ExcelProperty("鏂板鍚堝悓鏁拌揪鎴愮巼(%)")
+ private BigDecimal contractCountRate;
+
+ @Schema(description = "鐘舵��", example = "1")
+ @ExcelProperty(value = "鐘舵��", converter = DictConvert.class)
+ @DictFormat(DictTypeConstants.CRM_SALES_TARGET_STATUS)
+ private Integer status;
+
+ @Schema(description = "澶囨敞", example = "澶囨敞")
+ @ExcelProperty("澶囨敞")
+ private String remark;
+
+ @Schema(description = "鍒涘缓鏃堕棿", requiredMode = Schema.RequiredMode.REQUIRED)
+ @ExcelProperty("鍒涘缓鏃堕棿")
+ private LocalDateTime createTime;
+
+ @Schema(description = "鍒涘缓浜�", example = "1")
+ private String creator;
+ @Schema(description = "鍒涘缓浜哄悕绉�", example = "瓒呯骇绠$悊鍛�")
+ @ExcelProperty("鍒涘缓浜�")
+ private String creatorName;
+
+}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/CrmSalesTargetSaveReqVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/CrmSalesTargetSaveReqVO.java
new file mode 100644
index 0000000..a1b452b
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/CrmSalesTargetSaveReqVO.java
@@ -0,0 +1,65 @@
+package cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.crm.enums.salesTarget.CrmSalesTargetDimensionEnum;
+import cn.iocoder.yudao.module.crm.enums.salesTarget.CrmSalesTargetPeriodTypeEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Schema(description = "绠$悊鍚庡彴 - CRM 閿�鍞洰鏍囨柊澧�/淇敼 Request VO")
+@Data
+public class CrmSalesTargetSaveReqVO {
+
+ @Schema(description = "缂栧彿", example = "1")
+ private Long id;
+
+ @Schema(description = "鐩爣鍚嶇О", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026 涓婂崐骞撮攢鍞洰鏍�")
+ @NotEmpty(message = "鐩爣鍚嶇О涓嶈兘涓虹┖")
+ private String name;
+
+ @Schema(description = "鍛ㄦ湡绫诲瀷", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "鍛ㄦ湡绫诲瀷涓嶈兘涓虹┖")
+ @InEnum(CrmSalesTargetPeriodTypeEnum.class)
+ private Integer periodType;
+
+ @Schema(description = "鍛ㄦ湡鍊硷紝濡� 2026 / 2026-Q1 / 2026-07", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-Q1")
+ @NotEmpty(message = "鍛ㄦ湡鍊间笉鑳戒负绌�")
+ private String periodValue;
+
+ @Schema(description = "鐩爣缁村害", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "鐩爣缁村害涓嶈兘涓虹┖")
+ @InEnum(CrmSalesTargetDimensionEnum.class)
+ private Integer targetDimension;
+
+ @Schema(description = "鐩爣鐢ㄦ埛缂栧彿锛堢淮搴�=涓汉锛�", example = "1")
+ private Long targetUserId;
+
+ @Schema(description = "鐩爣閮ㄩ棬缂栧彿锛堢淮搴�=閮ㄩ棬锛�", example = "100")
+ private Long targetDeptId;
+
+ @Schema(description = "鐩爣鐗╂枡缂栧彿锛堢淮搴�=浜у搧锛屽叧鑱� MDM锛�", example = "1")
+ private Long targetItemId;
+
+ @Schema(description = "鍚堝悓閲戦鐩爣锛堝厓锛�", example = "1000000")
+ private BigDecimal contractTarget;
+
+ @Schema(description = "鍥炴閲戦鐩爣锛堝厓锛�", example = "800000")
+ private BigDecimal receivableTarget;
+
+ @Schema(description = "鍟嗘満閲戦鐩爣锛堝厓锛�", example = "1500000")
+ private BigDecimal businessTarget;
+
+ @Schema(description = "鏂板瀹㈡埛鏁扮洰鏍�", example = "50")
+ private Integer customerTarget;
+
+ @Schema(description = "鏂板鍚堝悓鏁扮洰鏍�", example = "30")
+ private Integer contractCountTarget;
+
+ @Schema(description = "澶囨敞", example = "澶囨敞")
+ private String remark;
+
+}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/analysis/CrmSalesTargetAnalysisReqVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/analysis/CrmSalesTargetAnalysisReqVO.java
new file mode 100644
index 0000000..fce1a5d
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/salesTarget/vo/analysis/CrmSalesTargetAnalysisReqVO.java
@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.analysis;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.crm.enums.salesTarget.CrmSalesTargetDimensionEnum;
+import cn.iocoder.yudao.module.crm.enums.salesTarget.CrmSalesTargetPeriodTypeEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "绠$悊鍚庡彴 - CRM 閿�鍞洰鏍囪揪鎴愬垎鏋� Request VO")
+@Data
+public class CrmSalesTargetAnalysisReqVO {
+
+ @Schema(description = "鍛ㄦ湡绫诲瀷", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "鍛ㄦ湡绫诲瀷涓嶈兘涓虹┖")
+ @InEnum(CrmSalesTargetPeriodTypeEnum.class)
+ private Integer periodType;
+
+ @Schema(description = "鍛ㄦ湡鍊硷紝濡� 2026 / 2026-Q1 / 2026-07", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026")
+ @NotEmpty(message = "鍛ㄦ湡鍊间笉鑳戒负绌�")
+ private String periodValue;
+
+ @Schema(description = "鐘舵�侊紝涓虹┖鍒欏叏閮�", example = "1")
+ private Integer status;
+
+ @Schema(description = "鐩爣缁村害锛屼负绌哄垯鍏ㄩ儴", example = "1")
+ @InEnum(CrmSalesTargetDimensionEnum.class)
+ private Integer targetDimension;
+
+}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/salesTarget/CrmSalesTargetDO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/salesTarget/CrmSalesTargetDO.java
new file mode 100644
index 0000000..5349b75
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/salesTarget/CrmSalesTargetDO.java
@@ -0,0 +1,107 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.salesTarget;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.crm.enums.DictTypeConstants;
+import cn.iocoder.yudao.module.crm.enums.salesTarget.CrmSalesTargetDimensionEnum;
+import cn.iocoder.yudao.module.crm.enums.salesTarget.CrmSalesTargetPeriodTypeEnum;
+import cn.iocoder.yudao.module.crm.enums.salesTarget.CrmSalesTargetStatusEnum;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * CRM 閿�鍞洰鏍� DO
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@TableName("crm_sales_target")
+@KeySequence("crm_sales_target_seq") // 鐢ㄤ簬 Oracle銆丳ostgreSQL銆並ingbase銆丏B2銆丠2 鏁版嵁搴撶殑涓婚敭鑷銆傚鏋滄槸 MySQL 绛夋暟鎹簱锛屽彲涓嶅啓銆�
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CrmSalesTargetDO extends BaseDO {
+
+ /**
+ * 缂栧彿
+ */
+ @TableId
+ private Long id;
+ /**
+ * 鐩爣鍚嶇О
+ */
+ private String name;
+ /**
+ * 鍛ㄦ湡绫诲瀷
+ *
+ * 瀛楀吀 {@link DictTypeConstants#CRM_SALES_TARGET_PERIOD_TYPE} / 鏋氫妇 {@link CrmSalesTargetPeriodTypeEnum}
+ */
+ private Integer periodType;
+ /**
+ * 鍛ㄦ湡鍊硷紝濡� 2026 / 2026-Q1 / 2026-07
+ */
+ private String periodValue;
+ /**
+ * 鍛ㄦ湡寮�濮嬫椂闂达紙淇濆瓨鏃剁敱鍛ㄦ湡璁$畻鍐欏叆锛�
+ */
+ private LocalDateTime startTime;
+ /**
+ * 鍛ㄦ湡缁撴潫鏃堕棿锛堜繚瀛樻椂鐢卞懆鏈熻绠楀啓鍏ワ級
+ */
+ private LocalDateTime endTime;
+ /**
+ * 鐩爣缁村害
+ *
+ * 瀛楀吀 {@link DictTypeConstants#CRM_SALES_TARGET_DIMENSION} / 鏋氫妇 {@link CrmSalesTargetDimensionEnum}
+ */
+ private Integer targetDimension;
+ /**
+ * 鐩爣鐢ㄦ埛缂栧彿锛堢淮搴�=涓汉锛�
+ */
+ private Long targetUserId;
+ /**
+ * 鐩爣閮ㄩ棬缂栧彿锛堢淮搴�=閮ㄩ棬锛�
+ */
+ private Long targetDeptId;
+ /**
+ * 鐩爣鐗╂枡缂栧彿锛堢淮搴�=浜у搧锛屽叧鑱� MDM锛�
+ */
+ private Long targetItemId;
+ /**
+ * 鍚堝悓閲戦鐩爣锛堝厓锛�
+ */
+ private BigDecimal contractTarget;
+ /**
+ * 鍥炴閲戦鐩爣锛堝厓锛�
+ */
+ private BigDecimal receivableTarget;
+ /**
+ * 鍟嗘満閲戦鐩爣锛堝厓锛�
+ */
+ private BigDecimal businessTarget;
+ /**
+ * 鏂板瀹㈡埛鏁扮洰鏍�
+ */
+ private Integer customerTarget;
+ /**
+ * 鏂板鍚堝悓鏁扮洰鏍�
+ */
+ private Integer contractCountTarget;
+ /**
+ * 鐘舵��
+ *
+ * 瀛楀吀 {@link DictTypeConstants#CRM_SALES_TARGET_STATUS} / 鏋氫妇 {@link CrmSalesTargetStatusEnum}
+ */
+ private Integer status;
+ /**
+ * 澶囨敞
+ */
+ private String remark;
+
+}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/salesTarget/CrmSalesTargetAchievementMapper.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/salesTarget/CrmSalesTargetAchievementMapper.java
new file mode 100644
index 0000000..b988284
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/salesTarget/CrmSalesTargetAchievementMapper.java
@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.salesTarget;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Collection;
+
+/**
+ * 閿�鍞洰鏍囪揪鎴愮粺璁� Mapper
+ *
+ * 缁熻瀹為檯杈炬垚閲戦/鏁伴噺锛屼粠 CRM 鐜版湁涓氬姟琛ㄦ寜鍛ㄦ湡鑼冨洿 + 褰掑睘浜猴紙鎴栫墿鏂欙級鑱氬悎銆�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Mapper
+public interface CrmSalesTargetAchievementMapper {
+
+ /**
+ * 鍚堝悓閲戦锛堜釜浜�/閮ㄩ棬缁村害锛�
+ */
+ BigDecimal selectContractPriceByUserIds(@Param("userIds") Collection<Long> userIds,
+ @Param("start") LocalDateTime start, @Param("end") LocalDateTime end);
+
+ /**
+ * 鍚堝悓鏁伴噺锛堜釜浜�/閮ㄩ棬缁村害锛�
+ */
+ Long selectContractCountByUserIds(@Param("userIds") Collection<Long> userIds,
+ @Param("start") LocalDateTime start, @Param("end") LocalDateTime end);
+
+ /**
+ * 鍥炴閲戦锛堜釜浜�/閮ㄩ棬缁村害锛�
+ */
+ BigDecimal selectReceivablePriceByUserIds(@Param("userIds") Collection<Long> userIds,
+ @Param("start") LocalDateTime start, @Param("end") LocalDateTime end);
+
+ /**
+ * 鍟嗘満閲戦锛堜釜浜�/閮ㄩ棬缁村害锛岃耽鍗曪級
+ */
+ BigDecimal selectBusinessPriceByUserIds(@Param("userIds") Collection<Long> userIds,
+ @Param("start") LocalDateTime start, @Param("end") LocalDateTime end);
+
+ /**
+ * 鏂板瀹㈡埛鏁帮紙涓汉/閮ㄩ棬缁村害锛�
+ */
+ Long selectCustomerCountByUserIds(@Param("userIds") Collection<Long> userIds,
+ @Param("start") LocalDateTime start, @Param("end") LocalDateTime end);
+
+ /**
+ * 鍚堝悓閲戦锛堜骇鍝佺淮搴︼紝鎸夊悎鍚屼骇鍝佹槑缁嗗綊灞炵墿鏂欙級
+ */
+ BigDecimal selectContractPriceByItemId(@Param("itemId") Long itemId,
+ @Param("start") LocalDateTime start, @Param("end") LocalDateTime end);
+
+ /**
+ * 鍚堝悓鏁伴噺锛堜骇鍝佺淮搴︼紝鎸夊悎鍚屼骇鍝佹槑缁嗗綊灞炵墿鏂欙級
+ */
+ Long selectContractCountByItemId(@Param("itemId") Long itemId,
+ @Param("start") LocalDateTime start, @Param("end") LocalDateTime end);
+
+}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/salesTarget/CrmSalesTargetMapper.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/salesTarget/CrmSalesTargetMapper.java
new file mode 100644
index 0000000..485d920
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/salesTarget/CrmSalesTargetMapper.java
@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.salesTarget;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.CrmSalesTargetPageReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.salesTarget.CrmSalesTargetDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 閿�鍞洰鏍� Mapper
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Mapper
+public interface CrmSalesTargetMapper extends BaseMapperX<CrmSalesTargetDO> {
+
+ default PageResult<CrmSalesTargetDO> selectPage(CrmSalesTargetPageReqVO reqVO) {
+ return selectPage(reqVO, buildQuery(reqVO));
+ }
+
+ default List<CrmSalesTargetDO> selectList(CrmSalesTargetPageReqVO reqVO) {
+ return selectList(buildQuery(reqVO));
+ }
+
+ private LambdaQueryWrapperX<CrmSalesTargetDO> buildQuery(CrmSalesTargetPageReqVO reqVO) {
+ return new LambdaQueryWrapperX<CrmSalesTargetDO>()
+ .likeIfPresent(CrmSalesTargetDO::getName, reqVO.getName())
+ .eqIfPresent(CrmSalesTargetDO::getPeriodType, reqVO.getPeriodType())
+ .eqIfPresent(CrmSalesTargetDO::getPeriodValue, reqVO.getPeriodValue())
+ .eqIfPresent(CrmSalesTargetDO::getTargetDimension, reqVO.getTargetDimension())
+ .eqIfPresent(CrmSalesTargetDO::getStatus, reqVO.getStatus())
+ .orderByDesc(CrmSalesTargetDO::getId);
+ }
+
+}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
index b38795f..1dbd1b9 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
@@ -16,5 +16,8 @@
String CRM_FOLLOW_UP_TYPE = "crm_follow_up_type"; // CRM 璺熻繘鏂瑰紡
String CRM_RECEIVABLE_RETURN_TYPE = "crm_receivable_return_type"; // CRM 鍥炴鏂瑰紡
String CRM_INVOICE_TYPE = "crm_invoice_type"; // CRM 鍙戠エ绫诲瀷
+ String CRM_SALES_TARGET_PERIOD_TYPE = "crm_sales_target_period_type"; // CRM 閿�鍞洰鏍囧懆鏈熺被鍨�
+ String CRM_SALES_TARGET_DIMENSION = "crm_sales_target_dimension"; // CRM 閿�鍞洰鏍囩淮搴�
+ String CRM_SALES_TARGET_STATUS = "crm_sales_target_status"; // CRM 閿�鍞洰鏍囩姸鎬�
}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 59f92fa..ffcdf9c 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -136,4 +136,8 @@
ErrorCode RECEIVABLE_CREATE_FAIL_INVOICE_CONTRACT_MISMATCH = new ErrorCode(1_020_016_011, "鍒涘缓鍥炴澶辫触锛屽師鍥狅細鍏宠仈鐨勫紑绁ㄤ笌鎵�閫夊悎鍚屼笉鍖归厤");
ErrorCode RECEIVABLE_CREATE_FAIL_INVOICE_PRICE_EXCEEDS = new ErrorCode(1_020_016_012, "鍒涘缓鍥炴澶辫触锛屽師鍥狅細鍥炴閲戦瓒呭嚭璇ュ紑绁ㄥ墿浣欏彲鍥炴閲戦锛岀洰鍓嶅彲鍥炴锛歿} 鍏�");
+ ErrorCode CRM_SALES_TARGET_NOT_EXISTS = new ErrorCode(1_020_017_000, "閿�鍞洰鏍囦笉瀛樺湪");
+ ErrorCode CRM_SALES_TARGET_PERIOD_VALUE_INVALID = new ErrorCode(1_020_017_001, "鍛ㄦ湡鍊笺�恵}銆戞牸寮忎笉姝g‘锛屽簲涓� {}");
+ ErrorCode CRM_SALES_TARGET_DIMENSION_TARGET_MISSING = new ErrorCode(1_020_017_002, "璇峰~鍐欑洰鏍噞}缂栧彿");
+
}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/salesTarget/CrmSalesTargetDimensionEnum.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/salesTarget/CrmSalesTargetDimensionEnum.java
new file mode 100644
index 0000000..4f03060
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/salesTarget/CrmSalesTargetDimensionEnum.java
@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.crm.enums.salesTarget;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 閿�鍞洰鏍囩殑鐩爣缁村害鏋氫妇
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@RequiredArgsConstructor
+@Getter
+public enum CrmSalesTargetDimensionEnum implements ArrayValuable<Integer> {
+
+ USER(1, "涓汉"),
+ DEPT(2, "閮ㄩ棬"),
+ ITEM(3, "浜у搧");
+
+ public static final Integer[] ARRAYS = Arrays.stream(values()).map(CrmSalesTargetDimensionEnum::getType).toArray(Integer[]::new);
+
+ /**
+ * 缁村害绫诲瀷
+ */
+ private final Integer type;
+ /**
+ * 缁村害鍚嶇О
+ */
+ private final String name;
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+ public static CrmSalesTargetDimensionEnum fromType(Integer type) {
+ return Arrays.stream(values())
+ .filter(value -> value.getType().equals(type))
+ .findFirst()
+ .orElse(null);
+ }
+
+}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/salesTarget/CrmSalesTargetPeriodTypeEnum.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/salesTarget/CrmSalesTargetPeriodTypeEnum.java
new file mode 100644
index 0000000..96678f4
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/salesTarget/CrmSalesTargetPeriodTypeEnum.java
@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.crm.enums.salesTarget;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 閿�鍞洰鏍囩殑鍛ㄦ湡绫诲瀷鏋氫妇
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@RequiredArgsConstructor
+@Getter
+public enum CrmSalesTargetPeriodTypeEnum implements ArrayValuable<Integer> {
+
+ YEAR(1, "骞村害"),
+ QUARTER(2, "瀛e害"),
+ MONTH(3, "鏈堝害");
+
+ public static final Integer[] ARRAYS = Arrays.stream(values()).map(CrmSalesTargetPeriodTypeEnum::getType).toArray(Integer[]::new);
+
+ /**
+ * 鍛ㄦ湡绫诲瀷
+ */
+ private final Integer type;
+ /**
+ * 鍛ㄦ湡鍚嶇О
+ */
+ private final String name;
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+ public static CrmSalesTargetPeriodTypeEnum fromType(Integer type) {
+ return Arrays.stream(values())
+ .filter(value -> value.getType().equals(type))
+ .findFirst()
+ .orElse(null);
+ }
+
+}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/salesTarget/CrmSalesTargetStatusEnum.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/salesTarget/CrmSalesTargetStatusEnum.java
new file mode 100644
index 0000000..c184f6b
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/salesTarget/CrmSalesTargetStatusEnum.java
@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.crm.enums.salesTarget;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 閿�鍞洰鏍囩殑鐘舵�佹灇涓�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@RequiredArgsConstructor
+@Getter
+public enum CrmSalesTargetStatusEnum implements ArrayValuable<Integer> {
+
+ NOT_ACTIVE(0, "鏈敓鏁�"),
+ ACTIVE(1, "宸茬敓鏁�"),
+ END(2, "宸茬粨鏉�"),
+ INVALID(3, "宸蹭綔搴�");
+
+ public static final Integer[] ARRAYS = Arrays.stream(values()).map(CrmSalesTargetStatusEnum::getStatus).toArray(Integer[]::new);
+
+ /**
+ * 鐘舵��
+ */
+ private final Integer status;
+ /**
+ * 鐘舵�佸悕绉�
+ */
+ private final String name;
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+ public static CrmSalesTargetStatusEnum fromStatus(Integer status) {
+ return Arrays.stream(values())
+ .filter(value -> value.getStatus().equals(status))
+ .findFirst()
+ .orElse(null);
+ }
+
+}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/salesTarget/CrmSalesTargetService.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/salesTarget/CrmSalesTargetService.java
new file mode 100644
index 0000000..f7f3064
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/salesTarget/CrmSalesTargetService.java
@@ -0,0 +1,81 @@
+package cn.iocoder.yudao.module.crm.service.salesTarget;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.CrmSalesTargetChangeStatusReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.CrmSalesTargetPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.CrmSalesTargetRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.CrmSalesTargetSaveReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.analysis.CrmSalesTargetAnalysisReqVO;
+import jakarta.validation.Valid;
+
+import java.util.List;
+
+/**
+ * CRM 閿�鍞洰鏍� Service 鎺ュ彛
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+public interface CrmSalesTargetService {
+
+ /**
+ * 鍒涘缓閿�鍞洰鏍�
+ *
+ * @param createReqVO 鍒涘缓淇℃伅
+ * @return 缂栧彿
+ */
+ Long createSalesTarget(@Valid CrmSalesTargetSaveReqVO createReqVO);
+
+ /**
+ * 鏇存柊閿�鍞洰鏍�
+ *
+ * @param updateReqVO 鏇存柊淇℃伅
+ */
+ void updateSalesTarget(@Valid CrmSalesTargetSaveReqVO updateReqVO);
+
+ /**
+ * 鍙樻洿閿�鍞洰鏍囩姸鎬侊紙浣滃簾/鎭㈠绛夛級
+ *
+ * @param reqVO 鐘舵�佸彉鏇翠俊鎭�
+ */
+ void changeSalesTargetStatus(@Valid CrmSalesTargetChangeStatusReqVO reqVO);
+
+ /**
+ * 鍒犻櫎閿�鍞洰鏍�
+ *
+ * @param id 缂栧彿
+ */
+ void deleteSalesTarget(Long id);
+
+ /**
+ * 鑾峰緱閿�鍞洰鏍囷紙鍚疄闄呰揪鎴愶級
+ *
+ * @param id 缂栧彿
+ * @return 閿�鍞洰鏍�
+ */
+ CrmSalesTargetRespVO getSalesTarget(Long id);
+
+ /**
+ * 鑾峰緱閿�鍞洰鏍囧垎椤碉紙鍚疄闄呰揪鎴愶級
+ *
+ * @param pageReqVO 鍒嗛〉鏌ヨ
+ * @return 閿�鍞洰鏍囧垎椤�
+ */
+ PageResult<CrmSalesTargetRespVO> getSalesTargetPage(CrmSalesTargetPageReqVO pageReqVO);
+
+ /**
+ * 鑾峰緱閿�鍞洰鏍囧垪琛紙鍏ㄩ噺锛岀敤浜� Excel 瀵煎嚭锛�
+ *
+ * @param reqVO 鏌ヨ鏉′欢
+ * @return 閿�鍞洰鏍囧垪琛�
+ */
+ List<CrmSalesTargetRespVO> getSalesTargetList(CrmSalesTargetPageReqVO reqVO);
+
+ /**
+ * 鑾峰緱閿�鍞洰鏍囪揪鎴愬垎鏋愬垪琛紙鎸夊懆鏈�+缁村害杩囨护锛屽惈瀹為檯杈炬垚涓庤揪鎴愮巼锛�
+ *
+ * @param reqVO 鍒嗘瀽鏌ヨ
+ * @return 杈炬垚鍒嗘瀽鍒楄〃
+ */
+ List<CrmSalesTargetRespVO> getSalesTargetAnalysis(CrmSalesTargetAnalysisReqVO reqVO);
+
+}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/salesTarget/CrmSalesTargetServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/salesTarget/CrmSalesTargetServiceImpl.java
new file mode 100644
index 0000000..9784da9
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/salesTarget/CrmSalesTargetServiceImpl.java
@@ -0,0 +1,368 @@
+package cn.iocoder.yudao.module.crm.service.salesTarget;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.CrmSalesTargetChangeStatusReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.CrmSalesTargetPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.CrmSalesTargetRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.CrmSalesTargetSaveReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.analysis.CrmSalesTargetAnalysisReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.salesTarget.CrmSalesTargetDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.salesTarget.CrmSalesTargetAchievementMapper;
+import cn.iocoder.yudao.module.crm.dal.mysql.salesTarget.CrmSalesTargetMapper;
+import cn.iocoder.yudao.module.crm.enums.salesTarget.CrmSalesTargetDimensionEnum;
+import cn.iocoder.yudao.module.crm.enums.salesTarget.CrmSalesTargetPeriodTypeEnum;
+import cn.iocoder.yudao.module.crm.enums.salesTarget.CrmSalesTargetStatusEnum;
+import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
+import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+
+/**
+ * 閿�鍞洰鏍� Service 瀹炵幇绫�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Service
+@Validated
+public class CrmSalesTargetServiceImpl implements CrmSalesTargetService {
+
+ private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
+
+ private static final Pattern YEAR_PATTERN = Pattern.compile("^\\d{4}$");
+ private static final Pattern QUARTER_PATTERN = Pattern.compile("^(\\d{4})-Q([1-4])$");
+ private static final Pattern MONTH_PATTERN = Pattern.compile("^(\\d{4})-(0[1-9]|1[0-2])$");
+ private static final DateTimeFormatter MONTH_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM");
+
+ @Resource
+ private CrmSalesTargetMapper salesTargetMapper;
+ @Resource
+ private CrmSalesTargetAchievementMapper achievementMapper;
+
+ @Resource
+ private AdminUserApi adminUserApi;
+ @Resource
+ private DeptApi deptApi;
+ @Resource
+ private MdmItemApi mdmItemApi;
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Long createSalesTarget(CrmSalesTargetSaveReqVO createReqVO) {
+ // 1. 鏍¢獙鐩爣缁村害瀵硅薄
+ validateTargetDimension(createReqVO);
+ // 2. 璁$畻鍛ㄦ湡璧锋锛屽苟鎻掑叆
+ CrmSalesTargetDO salesTarget = BeanUtils.toBean(createReqVO, CrmSalesTargetDO.class);
+ LocalDateTime[] period = calculatePeriod(createReqVO.getPeriodType(), createReqVO.getPeriodValue());
+ salesTarget.setStartTime(period[0]).setEndTime(period[1])
+ .setStatus(calcStatusByPeriod(period[0], period[1]));
+ salesTargetMapper.insert(salesTarget);
+ return salesTarget.getId();
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void updateSalesTarget(CrmSalesTargetSaveReqVO updateReqVO) {
+ // 1. 鏍¢獙瀛樺湪
+ CrmSalesTargetDO salesTarget = validateSalesTargetExists(updateReqVO.getId());
+ // 2. 鏍¢獙鐩爣缁村害瀵硅薄
+ validateTargetDimension(updateReqVO);
+ // 3. 鏇存柊
+ CrmSalesTargetDO updateDO = BeanUtils.toBean(updateReqVO, CrmSalesTargetDO.class);
+ LocalDateTime[] period = calculatePeriod(updateReqVO.getPeriodType(), updateReqVO.getPeriodValue());
+ updateDO.setStartTime(period[0]).setEndTime(period[1]);
+ // 鎵嬪姩浣滃簾鐨勭姸鎬佷笉鍥犵紪杈戣�岃鐩�
+ updateDO.setStatus(Objects.equals(salesTarget.getStatus(), CrmSalesTargetStatusEnum.INVALID.getStatus())
+ ? CrmSalesTargetStatusEnum.INVALID.getStatus() : calcStatusByPeriod(period[0], period[1]));
+ salesTargetMapper.updateById(updateDO);
+ }
+
+ @Override
+ public void changeSalesTargetStatus(CrmSalesTargetChangeStatusReqVO reqVO) {
+ // 1. 鏍¢獙瀛樺湪
+ validateSalesTargetExists(reqVO.getId());
+ // 2. 鏇存柊鐘舵��
+ salesTargetMapper.updateById(new CrmSalesTargetDO().setId(reqVO.getId()).setStatus(reqVO.getStatus()));
+ }
+
+ @Override
+ public void deleteSalesTarget(Long id) {
+ // 1. 鏍¢獙瀛樺湪
+ validateSalesTargetExists(id);
+ // 2. 鍒犻櫎
+ salesTargetMapper.deleteById(id);
+ }
+
+ @Override
+ public CrmSalesTargetRespVO getSalesTarget(Long id) {
+ CrmSalesTargetDO salesTarget = validateSalesTargetExists(id);
+ return convertListToRespVO(Collections.singletonList(salesTarget)).get(0);
+ }
+
+ @Override
+ public PageResult<CrmSalesTargetRespVO> getSalesTargetPage(CrmSalesTargetPageReqVO pageReqVO) {
+ PageResult<CrmSalesTargetDO> pageResult = salesTargetMapper.selectPage(pageReqVO);
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return PageResult.empty();
+ }
+ return new PageResult<>(convertListToRespVO(pageResult.getList()), pageResult.getTotal());
+ }
+
+ @Override
+ public List<CrmSalesTargetRespVO> getSalesTargetList(CrmSalesTargetPageReqVO reqVO) {
+ List<CrmSalesTargetDO> list = salesTargetMapper.selectList(reqVO);
+ return convertListToRespVO(list);
+ }
+
+ @Override
+ public List<CrmSalesTargetRespVO> getSalesTargetAnalysis(CrmSalesTargetAnalysisReqVO reqVO) {
+ // 鎸夊懆鏈� + 缁村害杩囨护鏌ヨ鍏ㄩ儴鐩爣锛堝惈宸茬粨鏉燂級锛岀敤浜庤揪鎴愬垎鏋愮湅鏉�
+ List<CrmSalesTargetDO> list = salesTargetMapper.selectList(new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX<CrmSalesTargetDO>()
+ .eqIfPresent(CrmSalesTargetDO::getStatus, reqVO.getStatus())
+ .eqIfPresent(CrmSalesTargetDO::getTargetDimension, reqVO.getTargetDimension())
+ .eq(CrmSalesTargetDO::getPeriodType, reqVO.getPeriodType())
+ .eq(CrmSalesTargetDO::getPeriodValue, reqVO.getPeriodValue())
+ .orderByDesc(CrmSalesTargetDO::getId));
+ return convertListToRespVO(list);
+ }
+
+ // ========== 绉佹湁鏂规硶 ==========
+
+ private CrmSalesTargetDO validateSalesTargetExists(Long id) {
+ CrmSalesTargetDO salesTarget = salesTargetMapper.selectById(id);
+ if (salesTarget == null) {
+ throw exception(CRM_SALES_TARGET_NOT_EXISTS);
+ }
+ return salesTarget;
+ }
+
+ private void validateTargetDimension(CrmSalesTargetSaveReqVO reqVO) {
+ if (Objects.equals(reqVO.getTargetDimension(), CrmSalesTargetDimensionEnum.USER.getType())
+ && reqVO.getTargetUserId() == null) {
+ throw exception(CRM_SALES_TARGET_DIMENSION_TARGET_MISSING, "鐢ㄦ埛");
+ }
+ if (Objects.equals(reqVO.getTargetDimension(), CrmSalesTargetDimensionEnum.DEPT.getType())
+ && reqVO.getTargetDeptId() == null) {
+ throw exception(CRM_SALES_TARGET_DIMENSION_TARGET_MISSING, "閮ㄩ棬");
+ }
+ if (Objects.equals(reqVO.getTargetDimension(), CrmSalesTargetDimensionEnum.ITEM.getType())
+ && reqVO.getTargetItemId() == null) {
+ throw exception(CRM_SALES_TARGET_DIMENSION_TARGET_MISSING, "鐗╂枡");
+ }
+ }
+
+ /**
+ * 鏍规嵁鍛ㄦ湡绫诲瀷 + 鍛ㄦ湡鍊艰绠楀懆鏈熻捣姝㈡椂闂达紙鍗婂紑鍖洪棿 [start, end)锛�
+ */
+ private LocalDateTime[] calculatePeriod(Integer periodType, String periodValue) {
+ if (Objects.equals(periodType, CrmSalesTargetPeriodTypeEnum.YEAR.getType())) {
+ if (!YEAR_PATTERN.matcher(periodValue).matches()) {
+ throw exception(CRM_SALES_TARGET_PERIOD_VALUE_INVALID, periodValue, "2026");
+ }
+ int year = Integer.parseInt(periodValue);
+ return new LocalDateTime[]{LocalDateTime.of(year, 1, 1, 0, 0), LocalDateTime.of(year + 1, 1, 1, 0, 0)};
+ }
+ if (Objects.equals(periodType, CrmSalesTargetPeriodTypeEnum.QUARTER.getType())) {
+ Matcher matcher = QUARTER_PATTERN.matcher(periodValue);
+ if (!matcher.matches()) {
+ throw exception(CRM_SALES_TARGET_PERIOD_VALUE_INVALID, periodValue, "2026-Q1");
+ }
+ int year = Integer.parseInt(matcher.group(1));
+ int startMonth = (Integer.parseInt(matcher.group(2)) - 1) * 3 + 1;
+ return new LocalDateTime[]{
+ YearMonth.of(year, startMonth).atDay(1).atStartOfDay(),
+ YearMonth.of(year, startMonth).plusMonths(3).atDay(1).atStartOfDay()};
+ }
+ if (Objects.equals(periodType, CrmSalesTargetPeriodTypeEnum.MONTH.getType())) {
+ if (!MONTH_PATTERN.matcher(periodValue).matches()) {
+ throw exception(CRM_SALES_TARGET_PERIOD_VALUE_INVALID, periodValue, "2026-07");
+ }
+ YearMonth yearMonth = YearMonth.parse(periodValue, MONTH_FORMAT);
+ return new LocalDateTime[]{
+ yearMonth.atDay(1).atStartOfDay(),
+ yearMonth.plusMonths(1).atDay(1).atStartOfDay()};
+ }
+ throw exception(CRM_SALES_TARGET_PERIOD_VALUE_INVALID, periodValue, "骞�/瀛e害/鏈�");
+ }
+
+ /**
+ * 鏍规嵁褰撳墠鏃堕棿涓庡懆鏈熻捣姝紝鑷姩鍒ゅ畾鐩爣鐘舵��
+ */
+ private Integer calcStatusByPeriod(LocalDateTime startTime, LocalDateTime endTime) {
+ LocalDateTime now = LocalDateTime.now();
+ if (now.isBefore(startTime)) {
+ return CrmSalesTargetStatusEnum.NOT_ACTIVE.getStatus();
+ }
+ if (now.isAfter(endTime)) {
+ return CrmSalesTargetStatusEnum.END.getStatus();
+ }
+ return CrmSalesTargetStatusEnum.ACTIVE.getStatus();
+ }
+
+ /**
+ * 灏� DO 鍒楄〃杞崲涓� RespVO 鍒楄〃锛屽~鍏呭璞″悕绉般�佸垱寤轰汉鍚嶇О涓庡疄闄呰揪鎴愭暟鎹�
+ */
+ private List<CrmSalesTargetRespVO> convertListToRespVO(List<CrmSalesTargetDO> targetList) {
+ if (CollUtil.isEmpty(targetList)) {
+ return Collections.emptyList();
+ }
+ // 1. 鏀堕泦鍏宠仈 ID锛屾壒閲忔煡璇㈠悕绉�
+ Set<Long> userIds = new HashSet<>();
+ Set<Long> deptIds = new HashSet<>();
+ Set<Long> itemIds = new HashSet<>();
+ for (CrmSalesTargetDO target : targetList) {
+ if (Objects.equals(target.getTargetDimension(), CrmSalesTargetDimensionEnum.USER.getType())) {
+ userIds.add(target.getTargetUserId());
+ } else if (Objects.equals(target.getTargetDimension(), CrmSalesTargetDimensionEnum.DEPT.getType())) {
+ deptIds.add(target.getTargetDeptId());
+ } else if (Objects.equals(target.getTargetDimension(), CrmSalesTargetDimensionEnum.ITEM.getType())) {
+ itemIds.add(target.getTargetItemId());
+ }
+ if (StrUtil.isNumeric(target.getCreator())) { // 鍒涘缓浜猴紙鐢ㄦ埛缂栧彿瀛楃涓诧級
+ userIds.add(Long.valueOf(target.getCreator()));
+ }
+ }
+ Map<Long, AdminUserRespDTO> userMap = CollUtil.isNotEmpty(userIds) ? adminUserApi.getUserMap(userIds) : Collections.emptyMap();
+ Map<Long, DeptRespDTO> deptMap = CollUtil.isNotEmpty(deptIds) ? deptApi.getDeptMap(deptIds) : Collections.emptyMap();
+ Map<Long, MdmItemRespDTO> itemMap = CollUtil.isNotEmpty(itemIds) ? mdmItemApi.getItemMap(itemIds) : Collections.emptyMap();
+
+ // 2. 杞崲骞跺~鍏�
+ return convertList(targetList, target -> {
+ CrmSalesTargetRespVO respVO = BeanUtils.toBean(target, CrmSalesTargetRespVO.class);
+ // 鐩爣瀵硅薄鍚嶇О
+ if (Objects.equals(target.getTargetDimension(), CrmSalesTargetDimensionEnum.USER.getType())) {
+ AdminUserRespDTO user = userMap.get(target.getTargetUserId());
+ if (user != null) {
+ respVO.setTargetUserName(user.getNickname());
+ }
+ } else if (Objects.equals(target.getTargetDimension(), CrmSalesTargetDimensionEnum.DEPT.getType())) {
+ DeptRespDTO dept = deptMap.get(target.getTargetDeptId());
+ if (dept != null) {
+ respVO.setTargetDeptName(dept.getName());
+ }
+ } else if (Objects.equals(target.getTargetDimension(), CrmSalesTargetDimensionEnum.ITEM.getType())) {
+ MdmItemRespDTO item = itemMap.get(target.getTargetItemId());
+ if (item != null) {
+ respVO.setTargetItemName(item.getName());
+ }
+ }
+ // 鍒涘缓浜哄悕绉�
+ if (StrUtil.isNumeric(target.getCreator())) {
+ AdminUserRespDTO creator = userMap.get(Long.valueOf(target.getCreator()));
+ if (creator != null) {
+ respVO.setCreatorName(creator.getNickname());
+ }
+ }
+ // 瀹為檯杈炬垚涓庤揪鎴愮巼
+ fillAchievement(target, respVO);
+ return respVO;
+ });
+ }
+
+ /**
+ * 璁$畻瀹為檯杈炬垚閲戦/鏁伴噺涓庤揪鎴愮巼
+ */
+ private void fillAchievement(CrmSalesTargetDO target, CrmSalesTargetRespVO respVO) {
+ if (Objects.equals(target.getTargetDimension(), CrmSalesTargetDimensionEnum.ITEM.getType())) {
+ // 浜у搧缁村害锛氫粎鍚堝悓閲戦涓庡悎鍚屾暟鍙寜鐗╂枡缁熻
+ BigDecimal contractPrice = achievementMapper.selectContractPriceByItemId(target.getTargetItemId(),
+ target.getStartTime(), target.getEndTime());
+ Long contractCount = achievementMapper.selectContractCountByItemId(target.getTargetItemId(),
+ target.getStartTime(), target.getEndTime());
+ respVO.setActualContractPrice(contractPrice);
+ respVO.setActualContractCount(contractCount);
+ respVO.setActualReceivablePrice(BigDecimal.ZERO);
+ respVO.setActualBusinessPrice(BigDecimal.ZERO);
+ respVO.setActualCustomerCount(0L);
+ } else {
+ Set<Long> userIds = resolveTargetUserIds(target);
+ if (CollUtil.isEmpty(userIds)) { // 閮ㄩ棬涓嬫棤鐢ㄦ埛
+ respVO.setActualContractPrice(BigDecimal.ZERO);
+ respVO.setActualContractCount(0L);
+ respVO.setActualReceivablePrice(BigDecimal.ZERO);
+ respVO.setActualBusinessPrice(BigDecimal.ZERO);
+ respVO.setActualCustomerCount(0L);
+ } else {
+ respVO.setActualContractPrice(achievementMapper.selectContractPriceByUserIds(userIds,
+ target.getStartTime(), target.getEndTime()));
+ respVO.setActualContractCount(achievementMapper.selectContractCountByUserIds(userIds,
+ target.getStartTime(), target.getEndTime()));
+ respVO.setActualReceivablePrice(achievementMapper.selectReceivablePriceByUserIds(userIds,
+ target.getStartTime(), target.getEndTime()));
+ respVO.setActualBusinessPrice(achievementMapper.selectBusinessPriceByUserIds(userIds,
+ target.getStartTime(), target.getEndTime()));
+ respVO.setActualCustomerCount(achievementMapper.selectCustomerCountByUserIds(userIds,
+ target.getStartTime(), target.getEndTime()));
+ }
+ }
+ // 杈炬垚鐜� = 瀹為檯 / 鐩爣 * 100
+ respVO.setContractRate(calcRate(respVO.getActualContractPrice(), target.getContractTarget()));
+ respVO.setReceivableRate(calcRate(respVO.getActualReceivablePrice(), target.getReceivableTarget()));
+ respVO.setBusinessRate(calcRate(respVO.getActualBusinessPrice(), target.getBusinessTarget()));
+ respVO.setCustomerRate(calcRate(respVO.getActualCustomerCount(), target.getCustomerTarget()));
+ respVO.setContractCountRate(calcRate(respVO.getActualContractCount(), target.getContractCountTarget()));
+ }
+
+ /**
+ * 瑙f瀽鐩爣鐨勭粺璁$敤鎴烽泦鍚堬細涓汉缁村害涓哄崟鐢ㄦ埛锛岄儴闂ㄧ淮搴︿负閮ㄩ棬鍙婂瓙閮ㄩ棬涓嬫墍鏈夌敤鎴�
+ */
+ private Set<Long> resolveTargetUserIds(CrmSalesTargetDO target) {
+ if (Objects.equals(target.getTargetDimension(), CrmSalesTargetDimensionEnum.USER.getType())) {
+ return target.getTargetUserId() == null ? Collections.emptySet() : Collections.singleton(target.getTargetUserId());
+ }
+ // 閮ㄩ棬缁村害锛氬惈瀛愰儴闂�
+ List<Long> deptIds = new ArrayList<>();
+ deptIds.add(target.getTargetDeptId());
+ List<DeptRespDTO> childDepts = deptApi.getChildDeptList(target.getTargetDeptId());
+ if (CollUtil.isNotEmpty(childDepts)) {
+ deptIds.addAll(convertList(childDepts, DeptRespDTO::getId));
+ }
+ return convertSet(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
+ }
+
+ private BigDecimal calcRate(BigDecimal actual, BigDecimal target) {
+ if (target == null || target.compareTo(BigDecimal.ZERO) <= 0) {
+ return null;
+ }
+ return (actual == null ? BigDecimal.ZERO : actual)
+ .multiply(HUNDRED).divide(target, 2, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal calcRate(Long actual, Integer target) {
+ if (target == null || target <= 0) {
+ return null;
+ }
+ return BigDecimal.valueOf(actual == null ? 0 : actual)
+ .multiply(HUNDRED).divide(BigDecimal.valueOf(target), 2, RoundingMode.HALF_UP);
+ }
+
+}
diff --git a/yudao-module-crm/src/main/resources/mapper/salesTarget/CrmSalesTargetAchievementMapper.xml b/yudao-module-crm/src/main/resources/mapper/salesTarget/CrmSalesTargetAchievementMapper.xml
new file mode 100644
index 0000000..d2033e4
--- /dev/null
+++ b/yudao-module-crm/src/main/resources/mapper/salesTarget/CrmSalesTargetAchievementMapper.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.salesTarget.CrmSalesTargetAchievementMapper">
+
+ <!-- 鍚堝悓閲戦锛堜釜浜�/閮ㄩ棬缁村害锛� -->
+ <select id="selectContractPriceByUserIds" resultType="java.math.BigDecimal">
+ SELECT IFNULL(SUM(total_price), 0) FROM crm_contract
+ WHERE deleted = 0
+ AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
+ AND order_date BETWEEN #{start} AND #{end}
+ AND owner_user_id IN
+ <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+ #{userId}
+ </foreach>
+ </select>
+
+ <!-- 鍚堝悓鏁伴噺锛堜釜浜�/閮ㄩ棬缁村害锛� -->
+ <select id="selectContractCountByUserIds" resultType="java.lang.Long">
+ SELECT COUNT(1) FROM crm_contract
+ WHERE deleted = 0
+ AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
+ AND order_date BETWEEN #{start} AND #{end}
+ AND owner_user_id IN
+ <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+ #{userId}
+ </foreach>
+ </select>
+
+ <!-- 鍥炴閲戦锛堜釜浜�/閮ㄩ棬缁村害锛� -->
+ <select id="selectReceivablePriceByUserIds" resultType="java.math.BigDecimal">
+ SELECT IFNULL(SUM(price), 0) FROM crm_receivable
+ WHERE deleted = 0
+ AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
+ AND return_time BETWEEN #{start} AND #{end}
+ AND owner_user_id IN
+ <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+ #{userId}
+ </foreach>
+ </select>
+
+ <!-- 鍟嗘満閲戦锛堜釜浜�/閮ㄩ棬缁村害锛屼粎璧㈠崟锛� -->
+ <select id="selectBusinessPriceByUserIds" resultType="java.math.BigDecimal">
+ SELECT IFNULL(SUM(total_price), 0) FROM crm_business
+ WHERE deleted = 0
+ AND end_status = ${@cn.iocoder.yudao.module.crm.enums.business.CrmBusinessEndStatusEnum@WIN.status}
+ AND deal_time BETWEEN #{start} AND #{end}
+ AND owner_user_id IN
+ <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+ #{userId}
+ </foreach>
+ </select>
+
+ <!-- 鏂板瀹㈡埛鏁帮紙涓汉/閮ㄩ棬缁村害锛� -->
+ <select id="selectCustomerCountByUserIds" resultType="java.lang.Long">
+ SELECT COUNT(1) FROM crm_customer
+ WHERE deleted = 0
+ AND create_time BETWEEN #{start} AND #{end}
+ AND owner_user_id IN
+ <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+ #{userId}
+ </foreach>
+ </select>
+
+ <!-- 鍚堝悓閲戦锛堜骇鍝佺淮搴︼紝鎸夊悎鍚屼骇鍝佹槑缁嗗綊灞炵墿鏂欙級 -->
+ <select id="selectContractPriceByItemId" resultType="java.math.BigDecimal">
+ SELECT IFNULL(SUM(cp.total_price), 0) FROM crm_contract c
+ INNER JOIN crm_contract_product cp ON cp.contract_id = c.id
+ WHERE c.deleted = 0 AND cp.deleted = 0
+ AND c.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
+ AND cp.item_id = #{itemId}
+ AND c.order_date BETWEEN #{start} AND #{end}
+ </select>
+
+ <!-- 鍚堝悓鏁伴噺锛堜骇鍝佺淮搴︼紝鎸夊悎鍚屼骇鍝佹槑缁嗗綊灞炵墿鏂欙級 -->
+ <select id="selectContractCountByItemId" resultType="java.lang.Long">
+ SELECT COUNT(DISTINCT c.id) FROM crm_contract c
+ INNER JOIN crm_contract_product cp ON cp.contract_id = c.id
+ WHERE c.deleted = 0 AND cp.deleted = 0
+ AND c.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
+ AND cp.item_id = #{itemId}
+ AND c.order_date BETWEEN #{start} AND #{end}
+ </select>
+
+</mapper>
--
Gitblit v1.9.3