2026-08-10 a378f38df1785cbc8c9f1d55ace6a37952d2f65b
feat(crm): 新增开票功能并完善权限查询

- 添加 CRM 开票功能枚举和编号前缀
- 完善权限查询方法,增加空集合判断防止 NPE
- 在回款服务中注入发票服务依赖
- 更新业务数据清理脚本,扩展清理范围到基础数据
- 新增 JHHG 环境配置文件,包含数据库、Redis、消息队列等完整配置
已修改22个文件
已添加17个文件
2421 ■■■■■ 文件已修改
docs/crm_invoice_integration.md 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/hrm_employee_import.md 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/clean_business_data.sql 248 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/crm_invoice_init.sql 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/hrm_employee_import.sql 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/CrmInvoiceController.java 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/vo/invoice/CrmInvoicePageReqVO.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/vo/invoice/CrmInvoiceRespVO.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/vo/invoice/CrmInvoiceSaveReqVO.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/invoice/CrmInvoiceDO.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/invoice/CrmInvoiceMapper.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/redis/no/CrmNoRedisDAO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmBizTypeEnum.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceService.java 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java 300 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/listener/CrmInvoiceStatusListener.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeImportExcelVO.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeImportReqVO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeImportRespVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeRespVO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeSaveReqVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/HrmEmployeeDO.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeService.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/dept/DeptApi.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/dept/DeptApiImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApi.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApiImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApi.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApiImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-jhhg.yaml 324 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/crm_invoice_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,129 @@
# CRM å¼€ç¥¨åŠŸèƒ½ - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- æ–°å¢žï¼š`views/crm/invoice/index.vue`(开票列表)、`views/crm/invoice/modules/form.vue`(开票表单)、`views/crm/invoice/detail/index.vue`(开票详情)
- æ–°å¢žï¼š`src/api/crm/invoice/index.ts`(开票接口)
- ä¿®æ”¹ï¼š`views/crm/receivable/modules/form.vue`(回款表单增加「先开票后回款」提示与校验)
- ä¿®æ”¹ï¼šå›žæ¬¾åˆ—表/详情(展示开票情况提示,或跳转开票)
> åŽç«¯èœå•已新增「营销管理 -> å¼€ç¥¨ç®¡ç†ã€ï¼ˆcomponent = `crm/invoice/index`),前端只需创建对应目录与页面组件。
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. åˆåŒå®¡æ‰¹é€šè¿‡ -> åˆ›å»ºå¼€ç¥¨ï¼šå¼€ç¥¨æ—¶é€‰æ‹©åˆåŒï¼Œå®¢æˆ·ã€åˆåŒåç§°ä»ŽåˆåŒå¸¦å…¥
2. å¼€ç¥¨æäº¤å®¡æ‰¹ -> å®¡æ‰¹é€šè¿‡ï¼šå®¡æ‰¹é€šè¿‡åŽå¼€ç¥¨æ‰ç”Ÿæ•ˆï¼Œæ‰å…è®¸å‚与回款校验
3. å®¡æ‰¹é€šè¿‡ -> ä¸Šä¼ å‘票附件:通过附件机制上传发票扫描件/电子票
4. å‘票已上传附件 -> åˆ›å»ºå›žæ¬¾ï¼šå›žæ¬¾åˆ›å»ºæ—¶åŽç«¯æ ¡éªŒã€ŒåˆåŒç´¯è®¡å·²å¼€ç¥¨ä¸”已上传附件的金额 >= ç´¯è®¡å›žæ¬¾é‡‘额(含本次)」
## API
### å¼€ç¥¨æŽ¥å£
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/crm/invoice/create` | åˆ›å»ºå¼€ç¥¨ |
| PUT | `/crm/invoice/update` | æ›´æ–°å¼€ç¥¨ï¼ˆä»…草稿/审批中) |
| DELETE | `/crm/invoice/delete` | åˆ é™¤å¼€ç¥¨ï¼ˆå®¡æ‰¹é€šè¿‡ä¸å¯åˆ ï¼‰ |
| GET | `/crm/invoice/get` | èŽ·å¾—å¼€ç¥¨è¯¦æƒ…ï¼ˆå«é™„ä»¶åˆ—è¡¨ï¼‰ |
| GET | `/crm/invoice/page` | èŽ·å¾—å¼€ç¥¨åˆ†é¡µ |
| GET | `/crm/invoice/page-by-customer` | åŸºäºŽå®¢æˆ·çš„开票分页 |
| GET | `/crm/invoice/export-excel` | å¯¼å‡ºå¼€ç¥¨ Excel |
| PUT | `/crm/invoice/submit` | æäº¤å¼€ç¥¨å®¡æ‰¹ |
| GET | `/crm/invoice/audit-count` | å¾…审核开票数量 |
**创建/更新请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | å¦ | æ›´æ–°æ—¶å¿…å¡« |
| contractId | Long | æ˜¯ | åˆåŒç¼–号,选择合同后客户自动带入 |
| ownerUserId | Long | å¦ | è´Ÿè´£äººï¼Œé»˜è®¤å½“前用户 |
| invoiceType | Integer | æ˜¯ | å‘票类型,字典 `crm_invoice_type`(1专用 2普通 3电子) |
| invoiceNo | String | å¦ | å‘票号码 |
| invoiceTitle | String | å¦ | å‘票抬头 |
| price | BigDecimal | æ˜¯ | å¼€ç¥¨é‡‘额(元) |
| invoiceTime | LocalDateTime | æ˜¯ | å¼€ç¥¨æ—¥æœŸ |
| remark | String | å¦ | å¤‡æ³¨ |
| blobIds | Long[] | å¦ | å‘票附件 BlobId åˆ—表 |
**响应字段新增:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| no | String | å¼€ç¥¨ç¼–号(KP+日期+序号,保存时后端生成) |
| customerId / customerName | Long / String | å®¢æˆ· |
| contract | CrmContractRespVO | åˆåŒä¿¡æ¯ |
| ownerUserName / ownerUserDeptName | String | è´Ÿè´£äºº/部门 |
| auditStatus | Integer | å®¡æ‰¹çŠ¶æ€ï¼ˆå­—å…¸ `crm_audit_status`) |
| attachmentList | StorageBlobRespDTO[] | å‘票附件列表 |
| processInstanceId | String | å·¥ä½œæµç¼–号 |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "id": 1,
    "no": "KP20260810000001",
    "contractId": 100,
    "contract": { "id": 100, "no": "HT2026...", "name": "..." },
    "customerId": 200,
    "customerName": "XX科技",
    "invoiceType": 1,
    "invoiceNo": "02564178",
    "invoiceTitle": "XX科技有限公司",
    "price": 9000.00,
    "invoiceTime": "2026-08-10",
    "auditStatus": 0,
    "attachmentList": [{ "id": 1, "url": "...", "name": "发票.pdf" }]
  }
}
```
### é™„件接口(沿用 system æ¨¡å—)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/system/storage-blob/upload` | ä¸Šä¼ å‘票文件,获得 `blobId` |
| POST | `/system/storage-attachment/bind` | ç»‘定附件到开票(`recordType=crm_invoice`、`recordId=开票id`) |
| GET | `/system/storage-attachment/list` | æŸ¥è¯¢å¼€ç¥¨é™„件列表 |
| DELETE | `/system/storage-attachment/delete` | åˆ é™¤é™„ä»¶ |
> ä¸Šä¼ ä¸Žä¿å­˜åˆ†ç¦»ï¼šè¡¨å•先选文件上传拿到 `blobId`,随创建/更新接口一起提交 `blobIds`;后端绑定。详情接口返回的 `attachmentList` å¯ç›´æŽ¥å±•示/预览。
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| no å¼€ç¥¨ç¼–号 | åˆ—表、详情 | è·³è½¬è¯¦æƒ… |
| customerName å®¢æˆ· | åˆ—表、详情 | è·³è½¬å®¢æˆ·è¯¦æƒ… |
| contract åˆåŒ | åˆ—表、详情 | è·³è½¬åˆåŒè¯¦æƒ… |
| invoiceType å‘票类型 | åˆ—表、详情 | å­—典展示 |
| invoiceNo å‘票号码 | åˆ—表、详情 | - |
| price å¼€ç¥¨é‡‘额 | åˆ—表、详情 | é‡‘额格式化 |
| invoiceTime å¼€ç¥¨æ—¥æœŸ | åˆ—表、详情 | æ—¥æœŸæ ¼å¼åŒ– |
| auditStatus å®¡æ‰¹çŠ¶æ€ | åˆ—表、详情 | å­—å…¸ `crm_audit_status` |
| attachmentList å‘票附件 | è¯¦æƒ… | é™„件列表,可预览/下载 |
| ownerUserName è´Ÿè´£äºº | åˆ—表 | - |
| remark å¤‡æ³¨ | åˆ—表、详情 | - |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| åˆ›å»ºå›žæ¬¾ | åŽç«¯æ ¡éªŒï¼šåŒåˆåŒã€Œå®¡æ‰¹é€šè¿‡ä¸”已上传发票附件」的累计开票金额 >= ç´¯è®¡å›žæ¬¾é‡‘额(含本次),否则报错「合同累计开票金额不足,目前可回款 X å…ƒã€ |
| å¼€ç¥¨é‡‘额 | åŽç«¯æ ¡éªŒï¼šå¼€ç¥¨é‡‘额不能超过合同金额减去已审批通过的开票金额 |
| å¼€ç¥¨ç¼–辑 | ä»…草稿、审批中可编辑;客户、合同、负责人不可修改 |
| å¼€ç¥¨åˆ é™¤ | å®¡æ‰¹é€šè¿‡åŽä¸å¯åˆ é™¤ |
| å¼€ç¥¨å®¡æ‰¹ | æäº¤åŽèµ° BPM æµç¨‹ï¼ˆkey=`crm-invoice-audit`),审批通过才生效 |
| å¾…办 | åŽç«¯ `audit-count` å¯ç”¨äºŽå¾…办红点 |
## æ³¨æ„äº‹é¡¹
- å‘票类型字典 `crm_invoice_type` åŽç«¯å·²é…ç½®ï¼Œå‰ç«¯éœ€åœ¨å¸¸é‡/字典中注册(1增值税专用 2增值税普通 3电子)
- èœå•权限:管理员需将新菜单(`crm:invoice:*`)分配给相应角色
- å›žæ¬¾è¡¨å•在用户选择合同后,可提示「请先完成开票并上传发票附件」,若未开票则创建回款会收到后端错误提示
- é™„件必须走 `/system/storage-*` æŽ¥å£ï¼Œ`recordType` å›ºå®šä¸º `crm_invoice`,禁止在业务表新增文件地址字段
- ä¸Šä¼ çš„发票文件建议限制图片/PDF æ ¼å¼
docs/hrm_employee_import.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,125 @@
# å‘˜å·¥ä¿¡æ¯å¯¼å…¥ - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- äººåŠ›èµ„æº > å‘˜å·¥ç®¡ç† åˆ—表页(`src/views/hrm/employee/index.vue`)
- å‘˜å·¥æ–°å¢ž/编辑弹窗(`src/views/hrm/employee/modules/form.vue`、`src/views/hrm/employee/data.ts`)
- å‘˜å·¥è¯¦æƒ…(`src/views/hrm/employee/modules/detail.vue`)
- æ–°å¢žï¼šå‘˜å·¥å¯¼å…¥å¼¹çª—(含下载模板、上传文件、展示导入结果)
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. **下载模板** -> å‰ç«¯è°ƒç”¨ `GET /hrm/employee/import-template` ä¸‹è½½ Excel æ¨¡æ¿ï¼Œæ¨¡æ¿åŒ…含示例行供参考。
2. **填写模板** -> ç”¨æˆ·æŒ‰æ¨¡æ¿å¡«å†™å‘˜å·¥ä¿¡æ¯ï¼Œå…¶ä¸­"部门名称/岗位名称/用户角色"三列填**名称**(用户角色多个用中文逗号分隔),由后端自动转换为 ID。
3. **上传导入** -> å‰ç«¯è°ƒç”¨ `POST /hrm/employee/import`(multipart è¡¨å•,file + updateSupport)上传文件。
4. **展示结果** -> åŽç«¯è¿”回导入结果(创建成功名单 / æ›´æ–°æˆåŠŸåå• / å¤±è´¥åå•及原因),前端逐条展示。
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | `/hrm/employee/import-template` | ä¸‹è½½å‘˜å·¥å¯¼å…¥æ¨¡æ¿ï¼ˆå«ç¤ºä¾‹æ•°æ®ï¼‰ |
| POST | `/hrm/employee/import` | å¯¼å…¥å‘˜å·¥ Excel |
### ä¸‹è½½æ¨¡æ¿
**响应:** Excel æ–‡ä»¶ï¼ˆ`员工导入模板.xls`),表头如下:
| å‘˜å·¥å§“名 | æ€§åˆ« | æ‰‹æœºå· | éƒ¨é—¨åç§° | å²—位名称 | ç”¨æˆ·è§’色 | å…¥èŒæ—¥æœŸ | å‘˜å·¥çŠ¶æ€ | èº«ä»½è¯å· | å‡ºç”Ÿæ—¥æœŸ | æ°‘族 | å©šå§»çŠ¶æ€ | å¹´é¾„ | å­¦åކ | æ”¿æ²»é¢è²Œ | é‚®ç®± | å¤‡æ³¨ |
|---------|------|--------|---------|---------|---------|---------|---------|---------|---------|------|---------|------|------|---------|------|------|
- æ€§åˆ«ï¼šå¡«"男"/"女"(字典 `hrm_gender`,值 1/2)
- å‘˜å·¥çŠ¶æ€ï¼šå¡«"在职"/"试用"/"离职"(字典 `hrm_employee_status`,值 1/2/3)
- ç”¨æˆ·è§’色:填角色名称,多个用**中文逗号**分隔(如"普通管理员,普通员工")
### å¯¼å…¥å‘˜å·¥
**请求参数(multipart/form-data):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| file | MultipartFile | æ˜¯ | ä¸Šä¼ çš„ Excel æ–‡ä»¶ |
| updateSupport | Boolean | å¦ | æ˜¯å¦æ”¯æŒæ›´æ–°ï¼Œé»˜è®¤ false。为 true æ—¶æ‰‹æœºå·å·²å­˜åœ¨çš„员工会被更新,否则记为失败 |
**响应:**
```json
{
  "code": 0,
  "data": {
    "createNames": ["张三", "李四"],
    "updateNames": ["王五"],
    "failureNames": {
      "赵六": "手机号已存在,请更换手机号",
      "孙七": "部门【不存在的部门】不存在"
    }
  }
}
```
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| createNames | List\<String\> | åˆ›å»ºæˆåŠŸçš„å‘˜å·¥å§“åæ•°ç»„ |
| updateNames | List\<String\> | æ›´æ–°æˆåŠŸçš„å‘˜å·¥å§“åæ•°ç»„ |
| failureNames | Map\<String, String\> | å¯¼å…¥å¤±è´¥é›†åˆï¼Œkey ä¸ºå‘˜å·¥å§“名,value ä¸ºå¤±è´¥åŽŸå›  |
## å­—段展示规则
### å¯¼å…¥æ¨¡æ¿ï¼ˆæ–°å¢žå­—段)
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| å‘˜å·¥å§“名 | æ¨¡æ¿åˆ— | å¿…å¡« |
| æ€§åˆ« | æ¨¡æ¿åˆ— | å¿…填,默认男 |
| æ‰‹æœºå· | æ¨¡æ¿åˆ— | å¿…填,将作为系统登录账号 |
| éƒ¨é—¨åç§° | æ¨¡æ¿åˆ— | å¿…填,填名称,后端转 ID |
| å²—位名称 | æ¨¡æ¿åˆ— | å¿…填,填名称,后端转 ID |
| ç”¨æˆ·è§’色 | æ¨¡æ¿åˆ— | é€‰å¡«ï¼Œå¡«åç§°ï¼Œå¤šä¸ªç”¨ä¸­æ–‡é€—号分隔 |
| å…¥èŒæ—¥æœŸ | æ¨¡æ¿åˆ— | å¿…填,格式 YYYY-MM-DD |
| å‘˜å·¥çŠ¶æ€ | æ¨¡æ¿åˆ— | å¿…填,默认在职 |
| èº«ä»½è¯å· | æ¨¡æ¿åˆ— | é€‰å¡« |
| å‡ºç”Ÿæ—¥æœŸ | æ¨¡æ¿åˆ— | é€‰å¡«ï¼Œæ ¼å¼ YYYY-MM-DD |
| æ°‘族 | æ¨¡æ¿åˆ— | é€‰å¡«ï¼Œæ–‡æœ¬ï¼ˆå¦‚"汉族") |
| å©šå§»çŠ¶æ€ | æ¨¡æ¿åˆ— | é€‰å¡«ï¼Œæ–‡æœ¬ï¼ˆå¦‚"已婚") |
| å¹´é¾„ | æ¨¡æ¿åˆ— | é€‰å¡«ï¼Œæ•°å­— |
| å­¦åކ | æ¨¡æ¿åˆ— | é€‰å¡«ï¼Œæ–‡æœ¬ï¼ˆå¦‚"本科") |
| æ”¿æ²»é¢è²Œ | æ¨¡æ¿åˆ— | é€‰å¡«ï¼Œæ–‡æœ¬ï¼ˆå¦‚"中共党员") |
| é‚®ç®± | æ¨¡æ¿åˆ— | é€‰å¡« |
| å¤‡æ³¨ | æ¨¡æ¿åˆ— | é€‰å¡« |
### æ–°å¢ž/编辑表单(新增字段)
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| æ°‘族 | è¡¨å• | æ–‡æœ¬è¾“入框 |
| å©šå§»çŠ¶æ€ | è¡¨å• | æ–‡æœ¬è¾“入框 |
| å¹´é¾„ | è¡¨å• | æ•°å­—输入框 |
| å­¦åކ | è¡¨å• | æ–‡æœ¬è¾“入框 |
| æ”¿æ²»é¢è²Œ | è¡¨å• | æ–‡æœ¬è¾“入框 |
### åˆ—表 / è¯¦æƒ… / å¯¼å‡º
- æ–°å¢žå­—段为纯文本,无需字典转码,可直接展示
- å¯¼å‡º Excel(`GET /hrm/employee/export-excel`)已包含上述字段列
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æ‰‹æœºå·å·²å­˜åœ¨ä¸” updateSupport=false | è¯¥è¡Œå¯¼å…¥å¤±è´¥ï¼ŒåŽŸå› "手机号已存在,请更换手机号" |
| æ‰‹æœºå·å·²å­˜åœ¨ä¸” updateSupport=true | æ›´æ–°è¯¥å‘˜å·¥ä¿¡æ¯ï¼ˆå«æ–°å¢žå­—段) |
| éƒ¨é—¨åç§° / å²—位名称填错或不存在 | è¯¥è¡Œå¯¼å…¥å¤±è´¥ï¼ŒåŽŸå› "部门【xx】不存在" / "岗位【xx】不存在" |
| ç”¨æˆ·è§’色名称填错或不存在 | è¯¥è¡Œå¯¼å…¥å¤±è´¥ï¼ŒåŽŸå› "角色【xx】不存在" |
| å‘˜å·¥å§“名、手机号、入职日期为空 | è¯¥è¡Œå¯¼å…¥å¤±è´¥ï¼ŒåŽŸå› "导入字段【xx】不能为空" |
| æ€§åˆ«ã€å‘˜å·¥çŠ¶æ€ä¸ºç©º | é»˜è®¤æŒ‰ç”· / åœ¨èŒå¤„理 |
| å¯¼å…¥æ—¶å‘˜å·¥ç¼–号 | ç”±ç³»ç»Ÿè‡ªåŠ¨ç”Ÿæˆï¼Œæ— éœ€åœ¨æ¨¡æ¿ä¸­å¡«å†™ |
## æ³¨æ„äº‹é¡¹
- å¯¼å…¥æŽ¥å£æƒé™ç ä¸º `hrm:employee:import`,需在菜单权限中配置。
- ç”¨æˆ·è§’色名称需与系统角色完全一致(精确匹配,不做模糊)。
- åŒä¸€è¡Œå†…,部门 / å²—位 / è§’色解析失败则该行整体失败,不会部分导入。
- å¯¼å…¥åˆ›å»ºå‘˜å·¥æ—¶ä¼šåŒæ­¥åˆ›å»ºç³»ç»Ÿç™»å½•账号(用户名为手机号,默认密码为手机号),请提醒使用方注意账号安全。
- æ¨¡æ¿ä¸­ç¤ºä¾‹è¡Œçš„部门 / å²—位 / è§’色名称为示例数据,正式导入前需替换为系统中真实存在的名称。
docs/sql/clean_business_data.sql
@@ -1,7 +1,8 @@
-- ============================================================
-- ç³»ç»Ÿä¸šåŠ¡æ•°æ®æ¸…ç†å­˜å‚¨è¿‡ç¨‹
--
-- ç”¨é€”:清空所有业务单据数据,保留系统配置和基础数据
-- ç”¨é€”:清空所有业务数据 + åŸºç¡€æ•°æ®ï¼ˆç‰©æ–™/客户/供应商/仓库等)
-- ä¿ç•™ï¼šç³»ç»Ÿè¡¨ã€å·¥ä½œæµå¼•擎、定时任务、编码规则、基础设施配置
-- æ³¨æ„ï¼šæ­¤æ“ä½œä¸å¯é€†ï¼Œæ‰§è¡Œå‰è¯·ç¡®è®¤å·²å¤‡ä»½æ•°æ®åº“
--
-- ä½¿ç”¨æ–¹å¼ï¼šCALL sp_clean_business_data();
@@ -13,11 +14,10 @@
CREATE PROCEDURE sp_clean_business_data()
BEGIN
    -- å…³é—­å¤–键检查,避免因表间引用导致删除失败
    SET FOREIGN_KEY_CHECKS = 0;
    -- ============================================================
    -- 1. ç³»ç»Ÿæ—¥å¿—与Token(可安全清理)
    -- 1. ç³»ç»Ÿæ—¥å¿—与Token
    -- ============================================================
    TRUNCATE TABLE system_login_log;
    TRUNCATE TABLE system_operate_log;
@@ -31,7 +31,7 @@
    TRUNCATE TABLE infra_job_log;
    -- ============================================================
    -- 2. æ–‡ä»¶ä¸Žé™„件(业务文件,清理后不影响系统运行)
    -- 2. æ–‡ä»¶ä¸Žé™„ä»¶
    -- ============================================================
    TRUNCATE TABLE system_storage_attachment;
    TRUNCATE TABLE system_storage_blob;
@@ -62,28 +62,19 @@
    -- ============================================================
    -- 4. MES ä»“库模块 â€” ä¸šåŠ¡å•æ®
    -- ============================================================
    -- åº“存交易流水(底层,先清)
    TRUNCATE TABLE mes_wm_transaction;
    TRUNCATE TABLE mes_wm_stock_reserve;
    TRUNCATE TABLE mes_wm_material_stock;
    -- åºåˆ—号 / æ‰¹æ¬¡ / æ¡ç 
    TRUNCATE TABLE mes_wm_sn;
    TRUNCATE TABLE mes_wm_batch;
    TRUNCATE TABLE mes_wm_barcode;
    -- ç›˜ç‚¹
    TRUNCATE TABLE mes_wm_stock_taking_task_result;
    TRUNCATE TABLE mes_wm_stock_taking_task_line;
    TRUNCATE TABLE mes_wm_stock_taking_task;
    TRUNCATE TABLE mes_wm_stock_taking_plan_param;
    TRUNCATE TABLE mes_wm_stock_taking_plan;
    -- åŒ…装
    TRUNCATE TABLE mes_wm_package_line;
    TRUNCATE TABLE mes_wm_package;
    -- å‡ºå…¥åº“明细/行(先清 detail/line,再清主表)
    TRUNCATE TABLE mes_wm_item_receipt_detail;
    TRUNCATE TABLE mes_wm_item_receipt_line;
    TRUNCATE TABLE mes_wm_item_receipt;
@@ -132,7 +123,7 @@
    TRUNCATE TABLE mes_wm_arrival_notice;
    -- ============================================================
    -- 5. MES è´¨æ£€æ¨¡å— â€” ä¸šåŠ¡æ•°æ®
    -- 5. MES è´¨æ£€æ¨¡å— â€” ä¸šåŠ¡å•æ®
    -- ============================================================
    TRUNCATE TABLE mes_qc_indicator_result_detail;
    TRUNCATE TABLE mes_qc_indicator_result;
@@ -165,11 +156,100 @@
    TRUNCATE TABLE mes_pd_document;
    TRUNCATE TABLE mes_pd_project;
    -- ç¼–码生成记录(规则本身保留)
    -- ============================================================
    -- 8. MES åŸºç¡€æ•°æ® â€” ç‰©æ–™/产品/BOM/SOP/SIP
    -- ============================================================
    TRUNCATE TABLE mes_md_product_bom;
    TRUNCATE TABLE mes_md_product_sip;
    TRUNCATE TABLE mes_md_product_sop;
    TRUNCATE TABLE mes_md_item;
    TRUNCATE TABLE mes_md_item_batch_config;
    TRUNCATE TABLE mes_md_item_type;
    TRUNCATE TABLE mes_md_unit_measure;
    TRUNCATE TABLE mes_md_vendor;
    TRUNCATE TABLE mes_md_auto_code_record;
    -- ============================================================
    -- 8. CRM æ¨¡å— â€” ä¸šåŠ¡å•æ®
    -- 9. MES åŸºç¡€æ•°æ® â€” è½¦é—´/工作站/资源
    -- ============================================================
    TRUNCATE TABLE mes_md_workstation_machine;
    TRUNCATE TABLE mes_md_workstation_tool;
    TRUNCATE TABLE mes_md_workstation_worker;
    TRUNCATE TABLE mes_md_workstation;
    TRUNCATE TABLE mes_md_workshop;
    -- ============================================================
    -- 10. MES åŸºç¡€æ•°æ® â€” ç­ç»„/排班
    -- ============================================================
    TRUNCATE TABLE mes_cal_plan_team;
    TRUNCATE TABLE mes_cal_plan_shift;
    TRUNCATE TABLE mes_cal_plan;
    TRUNCATE TABLE mes_cal_team_shift;
    TRUNCATE TABLE mes_cal_team_member;
    TRUNCATE TABLE mes_cal_team;
    TRUNCATE TABLE mes_cal_holiday;
    -- ============================================================
    -- 11. MES åŸºç¡€æ•°æ® â€” è®¾å¤‡/工具台账
    -- ============================================================
    TRUNCATE TABLE mes_dv_check_plan_subject;
    TRUNCATE TABLE mes_dv_check_plan_machinery;
    TRUNCATE TABLE mes_dv_check_plan;
    TRUNCATE TABLE mes_dv_machinery;
    TRUNCATE TABLE mes_dv_machinery_type;
    TRUNCATE TABLE mes_dv_subject;
    TRUNCATE TABLE mes_tm_tool;
    TRUNCATE TABLE mes_tm_tool_type;
    -- ============================================================
    -- 12. MES åŸºç¡€æ•°æ® â€” å·¥è‰ºè·¯çº¿/工序/参数
    -- ============================================================
    TRUNCATE TABLE mes_pro_route_product_bom;
    TRUNCATE TABLE mes_pro_route_product;
    TRUNCATE TABLE mes_pro_route_process;
    TRUNCATE TABLE mes_pro_route;
    TRUNCATE TABLE mes_pro_process_content;
    TRUNCATE TABLE mes_pro_process;
    TRUNCATE TABLE mes_pd_process_param_template_detail;
    TRUNCATE TABLE mes_pd_process_param_template;
    TRUNCATE TABLE mes_pd_process_param;
    -- ============================================================
    -- 13. MES åŸºç¡€æ•°æ® â€” è´¨æ£€æ–¹æ¡ˆ/缺陷/指标
    -- ============================================================
    TRUNCATE TABLE mes_qc_template_item;
    TRUNCATE TABLE mes_qc_template_indicator;
    TRUNCATE TABLE mes_qc_template;
    TRUNCATE TABLE mes_qc_defect;
    TRUNCATE TABLE mes_qc_indicator;
    -- ============================================================
    -- 14. MES åŸºç¡€æ•°æ® â€” ä»“库/库区/库位
    -- ============================================================
    TRUNCATE TABLE mes_wm_warehouse_location;
    TRUNCATE TABLE mes_wm_warehouse_area;
    TRUNCATE TABLE mes_wm_warehouse;
    -- ============================================================
    -- 15. MES åŸºç¡€é…ç½® â€” ç¼–码记录/条码/安灯配置
    -- ============================================================
    TRUNCATE TABLE mes_wm_barcode_config;
    TRUNCATE TABLE mes_pro_andon_config;
    -- ============================================================
    -- 16. MDM ä¸»æ•°æ®
    -- ============================================================
    TRUNCATE TABLE mdm_item_sku;
    TRUNCATE TABLE mdm_item_migration_map;
    TRUNCATE TABLE mdm_item_batch_config;
    TRUNCATE TABLE mdm_item;
    TRUNCATE TABLE mdm_item_category;
    TRUNCATE TABLE mdm_brand;
    TRUNCATE TABLE mdm_unit_measure;
    TRUNCATE TABLE mdm_warehouse;
    -- ============================================================
    -- 17. CRM æ¨¡å— â€” ä¸šåŠ¡å•æ® + å®¢æˆ· + é…ç½®
    -- ============================================================
    TRUNCATE TABLE after_sale_return_item;
    TRUNCATE TABLE after_sale_return;
@@ -191,9 +271,14 @@
    TRUNCATE TABLE crm_follow_up_record;
    TRUNCATE TABLE crm_customer;
    TRUNCATE TABLE crm_permission;
    TRUNCATE TABLE crm_business_status;
    TRUNCATE TABLE crm_business_status_type;
    TRUNCATE TABLE crm_contract_config;
    TRUNCATE TABLE crm_customer_limit_config;
    TRUNCATE TABLE crm_customer_pool_config;
    -- ============================================================
    -- 9. ERP æ¨¡å— â€” ä¸šåŠ¡å•æ®
    -- 18. ERP æ¨¡å— â€” ä¸šåŠ¡å•æ® + ä¾›åº”商 + ç»“算账户
    -- ============================================================
    TRUNCATE TABLE erp_finance_payment_item;
    TRUNCATE TABLE erp_finance_payment;
@@ -205,9 +290,31 @@
    TRUNCATE TABLE erp_purchase_request;
    TRUNCATE TABLE erp_sale_order_items;
    TRUNCATE TABLE erp_sale_order;
    TRUNCATE TABLE erp_supplier;
    TRUNCATE TABLE erp_account;
    -- ============================================================
    -- 10. HRM æ¨¡å— â€” ä¸šåŠ¡å•æ®
    -- 19. SRM æ¨¡å— â€” ä¸šåŠ¡å•æ® + ä¾›åº”商 + é…ç½®
    -- ============================================================
    TRUNCATE TABLE srm_bid_award;
    TRUNCATE TABLE srm_bid_evaluation;
    TRUNCATE TABLE srm_bid_open;
    TRUNCATE TABLE srm_tender_bid;
    TRUNCATE TABLE srm_tender_material;
    TRUNCATE TABLE srm_tender_project;
    TRUNCATE TABLE srm_delivery_plan;
    TRUNCATE TABLE srm_purchase_collaboration;
    TRUNCATE TABLE srm_supplier_quote;
    TRUNCATE TABLE srm_supplier_evaluation;
    TRUNCATE TABLE srm_supplier_certificate;
    TRUNCATE TABLE srm_supplier_contact;
    TRUNCATE TABLE srm_supplier_apply;
    TRUNCATE TABLE srm_supplier_category;
    TRUNCATE TABLE srm_supplier;
    TRUNCATE TABLE srm_tender_config;
    -- ============================================================
    -- 20. HRM æ¨¡å— â€” ä¸šåŠ¡å•æ® + é…ç½®
    -- ============================================================
    TRUNCATE TABLE hrm_salary_payment_detail;
    TRUNCATE TABLE hrm_salary_payment;
@@ -224,32 +331,27 @@
    TRUNCATE TABLE hrm_employee_social_security;
    TRUNCATE TABLE hrm_employee_salary;
    TRUNCATE TABLE hrm_employee;
    TRUNCATE TABLE hrm_attendance_holiday;
    TRUNCATE TABLE hrm_attendance_rule;
    TRUNCATE TABLE hrm_leave_type_config;
    TRUNCATE TABLE hrm_salary_structure;
    TRUNCATE TABLE hrm_social_security_scheme;
    TRUNCATE TABLE hrm_tax_rate_config;
    -- ============================================================
    -- 11. SRM æ¨¡å— â€” ä¸šåŠ¡å•æ®
    -- ============================================================
    TRUNCATE TABLE srm_bid_award;
    TRUNCATE TABLE srm_bid_evaluation;
    TRUNCATE TABLE srm_bid_open;
    TRUNCATE TABLE srm_tender_bid;
    TRUNCATE TABLE srm_tender_material;
    TRUNCATE TABLE srm_tender_project;
    TRUNCATE TABLE srm_delivery_plan;
    TRUNCATE TABLE srm_purchase_collaboration;
    TRUNCATE TABLE srm_supplier_quote;
    TRUNCATE TABLE srm_supplier_evaluation;
    TRUNCATE TABLE srm_supplier_certificate;
    TRUNCATE TABLE srm_supplier_contact;
    TRUNCATE TABLE srm_supplier_apply;
    -- ============================================================
    -- 12. BPM æ¨¡å— â€” ä¸šåŠ¡å•æ®
    -- 21. BPM æ¨¡å— â€” ä¸šåŠ¡æ•°æ® + æµç¨‹å®šä¹‰
    -- ============================================================
    TRUNCATE TABLE bpm_oa_leave;
    TRUNCATE TABLE bpm_process_instance_copy;
    TRUNCATE TABLE bpm_process_definition_info;
    TRUNCATE TABLE bpm_process_expression;
    TRUNCATE TABLE bpm_process_listener;
    TRUNCATE TABLE bpm_user_group;
    TRUNCATE TABLE bpm_form;
    TRUNCATE TABLE bpm_category;
    -- ============================================================
    -- 13. AI æ¨¡å— â€” ä¼šè¯/内容数据
    -- 22. AI æ¨¡å— â€” æ•°æ® + é…ç½®
    -- ============================================================
    TRUNCATE TABLE ai_chat_message;
    TRUNCATE TABLE ai_chat_conversation;
@@ -261,9 +363,13 @@
    TRUNCATE TABLE ai_music;
    TRUNCATE TABLE ai_write;
    TRUNCATE TABLE ai_workflow;
    TRUNCATE TABLE ai_api_key;
    TRUNCATE TABLE ai_model;
    TRUNCATE TABLE ai_tool;
    TRUNCATE TABLE ai_chat_role;
    -- ============================================================
    -- 14. IM æ¨¡å— â€” ä¼šè¯/消息数据
    -- 23. IM æ¨¡å— â€” æ•°æ® + é…ç½®
    -- ============================================================
    TRUNCATE TABLE im_group_message_receipt;
    TRUNCATE TABLE im_group_message;
@@ -282,27 +388,79 @@
    TRUNCATE TABLE im_rtc_participant;
    TRUNCATE TABLE im_rtc_call;
    TRUNCATE TABLE im_user_online;
    TRUNCATE TABLE im_sensitive_word;
    TRUNCATE TABLE im_face_user_item;
    TRUNCATE TABLE im_face_pack_item;
    TRUNCATE TABLE im_face_pack;
    -- ============================================================
    -- 15. OA é€šçŸ¥å…¬å‘Š â€” ä¸šåŠ¡æ•°æ®
    -- 24. BI æŠ¥è¡¨é…ç½®
    -- ============================================================
    TRUNCATE TABLE bi_warehouse_coordinate;
    TRUNCATE TABLE bi_chart_config;
    TRUNCATE TABLE bi_dashboard;
    TRUNCATE TABLE bi_data_source_config;
    -- ============================================================
    -- 25. å…¶ä»–模块数据
    -- ============================================================
    TRUNCATE TABLE oa_notice_read;
    TRUNCATE TABLE oa_notice_user;
    TRUNCATE TABLE oa_notice;
    TRUNCATE TABLE system_notice;
    TRUNCATE TABLE system_notify_template;
    TRUNCATE TABLE system_mail_log;
    TRUNCATE TABLE system_mail_template;
    TRUNCATE TABLE system_mail_account;
    TRUNCATE TABLE system_sms_code;
    TRUNCATE TABLE system_sms_log;
    TRUNCATE TABLE system_sms_template;
    TRUNCATE TABLE system_sms_channel;
    TRUNCATE TABLE system_social_user_bind;
    TRUNCATE TABLE system_social_user;
    TRUNCATE TABLE system_social_client;
    TRUNCATE TABLE report_go_view_project;
    -- ============================================================
    -- æ¢å¤å¤–键检查
    -- ============================================================
    SET FOREIGN_KEY_CHECKS = 1;
    -- è¿”回清理结果
    SELECT '业务数据清理完成' AS result;
END$$
DELIMITER ;
-- ============================================================
-- æ‰§è¡Œç¤ºä¾‹ï¼š
--   CALL sp_clean_business_data();
-- ä¿ç•™çš„表(不会被清理):
--
-- ã€ç³»ç»Ÿæ ¸å¿ƒã€‘
--   system_users / system_dept / system_post
--   system_role / system_role_menu
--   system_menu
--   system_dict_type / system_dict_data
--   system_tenant / system_tenant_package
--   system_user_post / system_user_role
--   system_oauth2_client
--   system_notify_template â†’ å·²æ¸…理
--
-- ã€åŸºç¡€è®¾æ–½ã€‘
--   infra_config / infra_job
--   infra_codegen_table / infra_codegen_column
--   infra_data_source_config / infra_file_config
--
-- ã€å·¥ä½œæµå¼•擎】
--   act_* (Activiti å¼•擎表)
--   flw_* (Flowable å¼•擎表)
--
-- ã€å®šæ—¶ä»»åŠ¡ã€‘
--   qrtz_* (Quartz å¼•擎表)
--
-- ã€ç¼–码规则(保留)】
--   mes_md_auto_code_rule
--   mes_md_auto_code_part
--
-- ã€ç¤ºä¾‹è¡¨ã€‘
--   yudao_demo01_contact
--   yudao_demo02_category
--   yudao_demo03_course / yudao_demo03_grade / yudao_demo03_student
-- ============================================================
-- æ‰§è¡Œï¼šCALL sp_clean_business_data();
docs/sql/crm_invoice_init.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
-- =====================================================
-- CRM å¼€ç¥¨åŠŸèƒ½åˆå§‹åŒ–è„šæœ¬
-- 1. å»ºè¡¨ crm_invoice
-- 2. æ•°æ®å­—å…¸ crm_invoice_type(发票类型)
-- 3. èœå•与按钮权限(营销管理 -> å¼€ç¥¨ç®¡ç†ï¼‰
-- =====================================================
-- 1. å»ºè¡¨
CREATE TABLE IF NOT EXISTS `crm_invoice` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `no` varchar(64) NOT NULL COMMENT '开票编号',
  `customer_id` bigint NOT NULL COMMENT '客户编号',
  `contract_id` bigint NOT NULL COMMENT '合同编号',
  `owner_user_id` bigint NOT NULL COMMENT '负责人编号',
  `invoice_type` int NOT NULL COMMENT '发票类型(字典 crm_invoice_type)',
  `invoice_no` varchar(64) DEFAULT NULL COMMENT '发票号码',
  `invoice_title` varchar(255) DEFAULT NULL COMMENT '发票抬头',
  `price` decimal(18,2) NOT NULL COMMENT '开票金额(元)',
  `invoice_time` datetime DEFAULT NULL COMMENT '开票日期',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  `process_instance_id` varchar(64) DEFAULT NULL COMMENT '工作流编号',
  `audit_status` int NOT NULL COMMENT '审批状态(crm_audit_status)',
  `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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM å¼€ç¥¨';
-- 2. æ•°æ®å­—典类型:crm_invoice_type
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`)
VALUES (1061500, 'CRM å‘票类型', 'crm_invoice_type', 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
  (1061600, 1, '增值税专用发票', '1', 'crm_invoice_type', 0, 'primary', '', '', '1', NOW(), '1', NOW(), b'0'),
  (1061601, 2, '增值税普通发票', '2', 'crm_invoice_type', 0, 'success', '', '', '1', NOW(), '1', NOW(), b'0'),
  (1061602, 3, '电子发票', '3', 'crm_invoice_type', 0, 'warning', '', '', '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 (1064000, '开票管理', '', 2, 59, 2617, 'invoice', '#', 'crm/invoice/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
  (1064001, '开票管理查询', 'crm:invoice:query', 3, 1, 1064000, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
  (1064002, '开票管理创建', 'crm:invoice:create', 3, 2, 1064000, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
  (1064003, '开票管理更新', 'crm:invoice:update', 3, 3, 1064000, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
  (1064004, '开票管理删除', 'crm:invoice:delete', 3, 4, 1064000, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
  (1064005, '开票管理导出', 'crm:invoice:export', 3, 5, 1064000, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
docs/sql/hrm_employee_import.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
-- å‘˜å·¥ä¿¡æ¯å¯¼å…¥åŠŸèƒ½ - æ–°å¢žå­—段
-- ç”¨äºŽ hrm_employee è¡¨ï¼šæ°‘族、婚姻状态、年龄、学历、政治面貌
ALTER TABLE hrm_employee ADD COLUMN nation VARCHAR(32) COMMENT '民族' AFTER id_card;
ALTER TABLE hrm_employee ADD COLUMN marriage_status VARCHAR(16) COMMENT '婚姻状态' AFTER nation;
ALTER TABLE hrm_employee ADD COLUMN age INT COMMENT '年龄' AFTER marriage_status;
ALTER TABLE hrm_employee ADD COLUMN education VARCHAR(32) COMMENT '学历' AFTER age;
ALTER TABLE hrm_employee ADD COLUMN political_status VARCHAR(32) COMMENT '政治面貌' AFTER education;
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/CrmInvoiceController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,188 @@
package cn.iocoder.yudao.module.crm.controller.admin.invoice;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
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.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoicePageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceSaveReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.invoice.CrmInvoiceDO;
import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
import cn.iocoder.yudao.module.crm.service.invoice.CrmInvoiceService;
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.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
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.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
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;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - CRM å¼€ç¥¨")
@RestController
@RequestMapping("/crm/invoice")
@Validated
public class CrmInvoiceController {
    @Resource
    private CrmInvoiceService invoiceService;
    @Resource
    private CrmContractService contractService;
    @Resource
    private CrmCustomerService customerService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private DeptApi deptApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @PostMapping("/create")
    @Operation(summary = "创建开票")
    @PreAuthorize("@ss.hasPermission('crm:invoice:create')")
    public CommonResult<Long> createInvoice(@Valid @RequestBody CrmInvoiceSaveReqVO createReqVO) {
        return success(invoiceService.createInvoice(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新开票")
    @PreAuthorize("@ss.hasPermission('crm:invoice:update')")
    public CommonResult<Boolean> updateInvoice(@Valid @RequestBody CrmInvoiceSaveReqVO updateReqVO) {
        invoiceService.updateInvoice(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除开票")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('crm:invoice:delete')")
    public CommonResult<Boolean> deleteInvoice(@RequestParam("id") Long id) {
        invoiceService.deleteInvoice(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得开票")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('crm:invoice:query')")
    public CommonResult<CrmInvoiceRespVO> getInvoice(@RequestParam("id") Long id) {
        CrmInvoiceDO invoice = invoiceService.getInvoice(id);
        return success(buildInvoiceDetail(invoice));
    }
    private CrmInvoiceRespVO buildInvoiceDetail(CrmInvoiceDO invoice) {
        if (invoice == null) {
            return null;
        }
        return buildInvoiceDetailList(Collections.singletonList(invoice)).get(0);
    }
    @GetMapping("/page")
    @Operation(summary = "获得开票分页")
    @PreAuthorize("@ss.hasPermission('crm:invoice:query')")
    public CommonResult<PageResult<CrmInvoiceRespVO>> getInvoicePage(@Valid CrmInvoicePageReqVO pageReqVO) {
        PageResult<CrmInvoiceDO> pageResult = invoiceService.getInvoicePage(pageReqVO, getLoginUserId());
        return success(new PageResult<>(buildInvoiceDetailList(pageResult.getList()), pageResult.getTotal()));
    }
    @GetMapping("/page-by-customer")
    @Operation(summary = "获得开票分页,基于指定客户")
    public CommonResult<PageResult<CrmInvoiceRespVO>> getInvoicePageByCustomer(@Valid CrmInvoicePageReqVO pageReqVO) {
        Assert.notNull(pageReqVO.getCustomerId(), "客户编号不能为空");
        PageResult<CrmInvoiceDO> pageResult = invoiceService.getInvoicePageByCustomerId(pageReqVO);
        return success(new PageResult<>(buildInvoiceDetailList(pageResult.getList()), pageResult.getTotal()));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出开票 Excel")
    @PreAuthorize("@ss.hasPermission('crm:invoice:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportInvoiceExcel(@Valid CrmInvoicePageReqVO exportReqVO,
                                   HttpServletResponse response) throws IOException {
        exportReqVO.setPageSize(PAGE_SIZE_NONE);
        List<CrmInvoiceDO> list = invoiceService.getInvoicePage(exportReqVO, getLoginUserId()).getList();
        // å¯¼å‡º Excel
        ExcelUtils.write(response, "开票.xls", "数据", CrmInvoiceRespVO.class,
                buildInvoiceDetailList(list));
    }
    private List<CrmInvoiceRespVO> buildInvoiceDetailList(List<CrmInvoiceDO> invoiceList) {
        if (CollUtil.isEmpty(invoiceList)) {
            return Collections.emptyList();
        }
        // 1.1 èŽ·å–å®¢æˆ·åˆ—è¡¨
        Map<Long, CrmCustomerDO> customerMap = customerService.getCustomerMap(
                convertSet(invoiceList, CrmInvoiceDO::getCustomerId));
        // 1.2 èŽ·å–åˆ›å»ºäººã€è´Ÿè´£äººåˆ—è¡¨
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(invoiceList,
                invoice -> Stream.of(NumberUtils.parseLong(invoice.getCreator()), invoice.getOwnerUserId())));
        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
        // 1.3 èŽ·å¾—åˆåŒåˆ—è¡¨
        Map<Long, CrmContractDO> contractMap = contractService.getContractMap(
                convertSet(invoiceList, CrmInvoiceDO::getContractId));
        // 2. æ‹¼æŽ¥ç»“æžœ
        return BeanUtils.toBean(invoiceList, CrmInvoiceRespVO.class, (invoiceVO) -> {
            // 2.1 æ‹¼æŽ¥å®¢æˆ·åç§°
            findAndThen(customerMap, invoiceVO.getCustomerId(), customer -> invoiceVO.setCustomerName(customer.getName()));
            // 2.2 æ‹¼æŽ¥è´Ÿè´£äººã€åˆ›å»ºäººåç§°
            MapUtils.findAndThen(userMap, NumberUtils.parseLong(invoiceVO.getCreator()),
                    user -> invoiceVO.setCreatorName(user.getNickname()));
            MapUtils.findAndThen(userMap, invoiceVO.getOwnerUserId(), user -> {
                invoiceVO.setOwnerUserName(user.getNickname());
                MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> invoiceVO.setOwnerUserDeptName(dept.getName()));
            });
            // 2.3 æ‹¼æŽ¥åˆåŒä¿¡æ¯
            findAndThen(contractMap, invoiceVO.getContractId(), contract ->
                    invoiceVO.setContract(BeanUtils.toBean(contract, CrmContractRespVO.class)));
            // 2.4 æ‹¼æŽ¥å‘票附件
            invoiceVO.setAttachmentList(storageAttachmentApi.listAttachments("crm_invoice", invoiceVO.getId()));
        });
    }
    @PutMapping("/submit")
    @Operation(summary = "提交开票审批")
    @PreAuthorize("@ss.hasPermission('crm:invoice:update')")
    public CommonResult<Boolean> submitInvoice(@RequestParam("id") Long id) {
        invoiceService.submitInvoice(id, getLoginUserId());
        return success(true);
    }
    @GetMapping("/audit-count")
    @Operation(summary = "获得待审核开票数量")
    @PreAuthorize("@ss.hasPermission('crm:invoice:query')")
    public CommonResult<Long> getAuditInvoiceCount() {
        return success(invoiceService.getAuditInvoiceCount(getLoginUserId()));
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/vo/invoice/CrmInvoicePageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum;
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 CrmInvoicePageReqVO extends PageParam {
    @Schema(description = "开票编号")
    private String no;
    @Schema(description = "客户编号", example = "4963")
    private Long customerId;
    @Schema(description = "合同编号", example = "4963")
    private Long contractId;
    @Schema(description = "场景类型", example = "1")
    @InEnum(CrmSceneTypeEnum.class)
    private Integer sceneType; // åœºæ™¯ç±»åž‹ï¼Œä¸º null æ—¶åˆ™è¡¨ç¤ºå…¨éƒ¨
    @Schema(description = "审批状态", example = "20")
    @InEnum(CrmAuditStatusEnum.class)
    private Integer auditStatus;
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/vo/invoice/CrmInvoiceRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
package cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractRespVO;
import cn.iocoder.yudao.module.crm.enums.DictTypeConstants;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
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;
import java.util.List;
@Schema(description = "管理后台 - CRM å¼€ç¥¨ Response VO")
@Data
@ExcelIgnoreUnannotated
public class CrmInvoiceRespVO {
    @Schema(description = "编号", example = "25787")
    @ExcelProperty("编号")
    private Long id;
    @Schema(description = "开票编号", example = "31177")
    @ExcelProperty("开票编号")
    private String no;
    @Schema(description = "发票类型", example = "1")
    @ExcelProperty(value = "发票类型", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.CRM_INVOICE_TYPE)
    private Integer invoiceType;
    @Schema(description = "发票号码", example = "02564178")
    @ExcelProperty("发票号码")
    private String invoiceNo;
    @Schema(description = "发票抬头", example = "XX科技有限公司")
    @ExcelProperty("发票抬头")
    private String invoiceTitle;
    @Schema(description = "开票金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "9000")
    @ExcelProperty("开票金额")
    private BigDecimal price;
    @Schema(description = "开票日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
    @ExcelProperty("开票日期")
    private LocalDateTime invoiceTime;
    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
    private Long customerId;
    @Schema(description = "客户名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "test")
    @ExcelProperty("客户名字")
    private String customerName;
    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
    @ExcelProperty("合同编号")
    private Long contractId;
    @Schema(description = "合同信息")
    private CrmContractRespVO contract;
    @Schema(description = "负责人的用户编号", example = "25682")
    private Long ownerUserId;
    @Schema(description = "负责人名字", example = "25682")
    @ExcelProperty("负责人名字")
    private String ownerUserName;
    @Schema(description = "负责人部门")
    @ExcelProperty("负责人部门")
    private String ownerUserDeptName;
    @Schema(description = "工作流编号", example = "1043")
    @ExcelProperty("工作流编号")
    private String processInstanceId;
    @Schema(description = "审批状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @ExcelProperty(value = "审批状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.CRM_AUDIT_STATUS)
    private Integer auditStatus;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
    private String remark;
    @Schema(description = "发票附件列表")
    private List<StorageBlobRespDTO> attachmentList;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
    @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("更新时间")
    private LocalDateTime updateTime;
    @Schema(description = "创建人", example = "25682")
    private String creator;
    @Schema(description = "创建人名字", example = "test")
    @ExcelProperty("创建人名字")
    private String creatorName;
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/vo/invoice/CrmInvoiceSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - CRM å¼€ç¥¨æ–°å¢ž/修改 Request VO")
@Data
public class CrmInvoiceSaveReqVO {
    @Schema(description = "编号", example = "25787")
    private Long id;
    @Schema(description = "开票编号", example = "31177")
    private String no;
    @Schema(description = "客户编号", example = "2")
    private Long customerId; // è¯¥å­—段不通过前端传递,而是 contractId æŸ¥è¯¢å‡ºæ¥è®¾ç½®è¿›åŽ»
    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @NotNull(message = "合同编号不能为空")
    private Long contractId;
    @Schema(description = "负责人编号", example = "25682")
    private Long ownerUserId;
    @Schema(description = "发票类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "发票类型不能为空")
    private Integer invoiceType;
    @Schema(description = "发票号码", example = "02564178")
    private String invoiceNo;
    @Schema(description = "发票抬头", example = "XX科技有限公司")
    private String invoiceTitle;
    @Schema(description = "开票金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "9000")
    @NotNull(message = "开票金额不能为空")
    private BigDecimal price;
    @Schema(description = "开票日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
    @NotNull(message = "开票日期不能为空")
    private LocalDateTime invoiceTime;
    @Schema(description = "备注", example = "备注")
    private String remark;
    @Schema(description = "发票附件 BlobId åˆ—表")
    private List<Long> blobIds;
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/invoice/CrmInvoiceDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,97 @@
package cn.iocoder.yudao.module.crm.dal.dataobject.invoice;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
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;
/**
 * å¼€ç¥¨ DO
 *
 * @author è¶…级管理员
 */
@TableName("crm_invoice")
@KeySequence("crm_invoice_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CrmInvoiceDO extends BaseDO {
    /**
     * ID
     */
    @TableId
    private Long id;
    /**
     * å¼€ç¥¨ç¼–号
     */
    private String no;
    /**
     * å®¢æˆ·ç¼–号
     *
     * å…³è” {@link CrmCustomerDO#getId()}
     */
    private Long customerId;
    /**
     * åˆåŒç¼–号
     *
     * å…³è” {@link CrmContractDO#getId()}
     */
    private Long contractId;
    /**
     * è´Ÿè´£äººç¼–号,关联 {@link AdminUserRespDTO#getId()}
     */
    private Long ownerUserId;
    /**
     * å‘票类型
     *
     * å­—å…¸ {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants#CRM_INVOICE_TYPE}
     */
    private Integer invoiceType;
    /**
     * å‘票号码
     */
    private String invoiceNo;
    /**
     * å‘票抬头
     */
    private String invoiceTitle;
    /**
     * å¼€ç¥¨é‡‘额,单位:元
     */
    private BigDecimal price;
    /**
     * å¼€ç¥¨æ—¥æœŸ
     */
    private LocalDateTime invoiceTime;
    /**
     * å¤‡æ³¨
     */
    private String remark;
    /**
     * å·¥ä½œæµç¼–号
     *
     * å…³è” ProcessInstance çš„ id å±žæ€§
     */
    private String processInstanceId;
    /**
     * å®¡æ‰¹çŠ¶æ€
     *
     * æžšä¸¾ {@link CrmAuditStatusEnum}
     */
    private Integer auditStatus;
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/invoice/CrmInvoiceMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,93 @@
package cn.iocoder.yudao.module.crm.dal.mysql.invoice;
import cn.hutool.core.collection.CollUtil;
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.framework.mybatis.core.query.MPJLambdaWrapperX;
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoicePageReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.invoice.CrmInvoiceDO;
import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum;
import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
 * å¼€ç¥¨ Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface CrmInvoiceMapper extends BaseMapperX<CrmInvoiceDO> {
    default CrmInvoiceDO selectByNo(String no) {
        return selectOne(CrmInvoiceDO::getNo, no);
    }
    default PageResult<CrmInvoiceDO> selectPageByCustomerId(CrmInvoicePageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<CrmInvoiceDO>()
                .eq(CrmInvoiceDO::getCustomerId, reqVO.getCustomerId()) // å¿…须传递
                .eqIfPresent(CrmInvoiceDO::getNo, reqVO.getNo())
                .eqIfPresent(CrmInvoiceDO::getContractId, reqVO.getContractId())
                .orderByDesc(CrmInvoiceDO::getId));
    }
    default PageResult<CrmInvoiceDO> selectPage(CrmInvoicePageReqVO pageReqVO, Long userId) {
        MPJLambdaWrapperX<CrmInvoiceDO> query = new MPJLambdaWrapperX<>();
        // æ‹¼æŽ¥æ•°æ®æƒé™çš„æŸ¥è¯¢æ¡ä»¶
        CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_INVOICE.getType(),
                CrmInvoiceDO::getId, userId, pageReqVO.getSceneType());
        // æ‹¼æŽ¥è‡ªèº«çš„æŸ¥è¯¢æ¡ä»¶
        query.selectAll(CrmInvoiceDO.class)
                .eqIfPresent(CrmInvoiceDO::getNo, pageReqVO.getNo())
                .eqIfPresent(CrmInvoiceDO::getContractId, pageReqVO.getContractId())
                .eqIfPresent(CrmInvoiceDO::getAuditStatus, pageReqVO.getAuditStatus())
                .orderByDesc(CrmInvoiceDO::getId);
        return selectJoinPage(pageReqVO, CrmInvoiceDO.class, query);
    }
    default Long selectCountByAudit(Long userId) {
        MPJLambdaWrapperX<CrmInvoiceDO> query = new MPJLambdaWrapperX<>();
        // æˆ‘负责的 + éžå…¬æµ·
        CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_INVOICE.getType(),
                CrmInvoiceDO::getId, userId, CrmSceneTypeEnum.OWNER.getType());
        // æœªå®¡æ ¸
        query.eq(CrmInvoiceDO::getAuditStatus, CrmAuditStatusEnum.PROCESS.getStatus());
        return selectCount(query);
    }
    default List<CrmInvoiceDO> selectListByContractIdAndStatus(Long contractId, Collection<Integer> auditStatuses) {
        return selectList(new LambdaQueryWrapperX<CrmInvoiceDO>()
                .eq(CrmInvoiceDO::getContractId, contractId)
                .in(CrmInvoiceDO::getAuditStatus, auditStatuses));
    }
    default Map<Long, BigDecimal> selectInvoicePriceMapByContractId(Collection<Long> contractIds) {
        if (CollUtil.isEmpty(contractIds)) {
            return Collections.emptyMap();
        }
        // SQL sum æŸ¥è¯¢
        List<Map<String, Object>> result = selectMaps(new QueryWrapper<CrmInvoiceDO>()
                .select("contract_id, SUM(price) AS total_price")
                .eq("audit_status", CrmAuditStatusEnum.APPROVE.getStatus())
                .groupBy("contract_id")
                .in("contract_id", contractIds));
        // èŽ·å¾—é‡‘é¢
        return convertMap(result, obj -> (Long) obj.get("contract_id"), obj -> (BigDecimal) obj.get("total_price"));
    }
    default Long selectCountByContractId(Long contractId) {
        return selectCount(CrmInvoiceDO::getContractId, contractId);
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
@@ -1,11 +1,13 @@
package cn.iocoder.yudao.module.crm.dal.mysql.permission;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
@@ -30,6 +32,9 @@
    }
    default List<CrmPermissionDO> selectByBizTypeAndBizIds(Integer bizType, Collection<Long> bizIds) {
        if (CollUtil.isEmpty(bizIds)) {
            return Collections.emptyList();
        }
        return selectList(new LambdaQueryWrapperX<CrmPermissionDO>()
                .eq(CrmPermissionDO::getBizType, bizType)
                .in(CrmPermissionDO::getBizId, bizIds));
@@ -67,6 +72,9 @@
    }
    default Long selectListByBiz(Collection<Integer> bizTypes, Collection<Long> bizIds, Collection<Long> userIds) {
        if (CollUtil.isEmpty(bizTypes) || CollUtil.isEmpty(bizIds) || CollUtil.isEmpty(userIds)) {
            return 0L;
        }
        return selectCount(new LambdaQueryWrapperX<CrmPermissionDO>()
                .in(CrmPermissionDO::getBizType, bizTypes)
                .in(CrmPermissionDO::getBizId, bizIds)
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/redis/no/CrmNoRedisDAO.java
@@ -34,6 +34,11 @@
     */
    public static final String SALE_QUOTATION_NO_PREFIX = "QT";
    /**
     * å¼€ç¥¨ {@link cn.iocoder.yudao.module.crm.dal.dataobject.invoice.CrmInvoiceDO}
     */
    public static final String INVOICE_PREFIX = "KP";
    @Resource
    private StringRedisTemplate stringRedisTemplate;
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
@@ -15,5 +15,6 @@
    String CRM_PRODUCT_STATUS = "crm_product_status"; // CRM äº§å“çŠ¶æ€
    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 å‘票类型
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -119,4 +119,15 @@
    ErrorCode SALE_QUOTATION_ITEM_NOT_EXISTS = new ErrorCode(1_020_015_005, "物料不存在或未启用");
    ErrorCode SALE_QUOTATION_CONTRACT_EXISTS = new ErrorCode(1_020_015_006, "销售报价单已转合同,请勿重复操作");
    // ========== å¼€ç¥¨ 1_020_016_000 ==========
    ErrorCode INVOICE_NOT_EXISTS = new ErrorCode(1_020_016_000, "开票不存在");
    ErrorCode INVOICE_NO_EXISTS = new ErrorCode(1_020_016_001, "生成开票序列号重复,请重试");
    ErrorCode INVOICE_UPDATE_FAIL_EDITING_PROHIBITED = new ErrorCode(1_020_016_002, "更新开票失败,原因:禁止编辑");
    ErrorCode INVOICE_DELETE_FAIL_IS_APPROVE = new ErrorCode(1_020_016_003, "删除开票失败,原因:开票审批已通过");
    ErrorCode INVOICE_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_020_016_004, "开票提交审核失败,原因:开票没处在未提交状态");
    ErrorCode INVOICE_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_020_016_005, "更新开票审核状态失败,原因:开票不是审核中状态");
    ErrorCode INVOICE_CREATE_FAIL_CONTRACT_NOT_APPROVE = new ErrorCode(1_020_016_006, "创建开票失败,原因:合同不是审核通过状态");
    ErrorCode INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT = new ErrorCode(1_020_016_007, "创建开票失败,原因:开票金额超出合同金额,目前可开票:{} å…ƒ");
    ErrorCode RECEIVABLE_CREATE_FAIL_INVOICE_NOT_ENOUGH = new ErrorCode(1_020_016_008, "创建回款失败,原因:合同累计开票金额不足,目前可回款:{} å…ƒ");
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java
@@ -160,4 +160,16 @@
    String CRM_RECEIVABLE_PLAN_DELETE_SUB_TYPE = "删除回款计划";
    String CRM_RECEIVABLE_PLAN_DELETE_SUCCESS = "删除了合同【{getContractById{#receivablePlan.contractId}}】的第【{{#receivablePlan.period}}】期回款计划";
    // ======================= CRM_INVOICE å¼€ç¥¨ =======================
    String CRM_INVOICE_TYPE = "CRM å¼€ç¥¨";
    String CRM_INVOICE_CREATE_SUB_TYPE = "创建开票";
    String CRM_INVOICE_CREATE_SUCCESS = "创建了合同【{getContractById{#invoice.contractId}}】的编号为【{{#invoice.no}}】的开票";
    String CRM_INVOICE_UPDATE_SUB_TYPE = "更新开票";
    String CRM_INVOICE_UPDATE_SUCCESS = "更新了合同【{getContractById{#invoice.contractId}}】的编号为【{{#invoice.no}}】的开票: {_DIFF{#updateReqVO}}";
    String CRM_INVOICE_DELETE_SUB_TYPE = "删除开票";
    String CRM_INVOICE_DELETE_SUCCESS = "删除了合同【{getContractById{#invoice.contractId}}】的编号为【{{#invoice.no}}】的开票";
    String CRM_INVOICE_SUBMIT_SUB_TYPE = "提交开票审批";
    String CRM_INVOICE_SUBMIT_SUCCESS = "提交编号为【{{#invoiceNo}}】的开票审批成功";
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmBizTypeEnum.java
@@ -25,7 +25,8 @@
    CRM_PRODUCT(6, "产品"),
    CRM_RECEIVABLE(7, "回款"),
    CRM_RECEIVABLE_PLAN(8, "回款计划"),
    CRM_SALE_QUOTATION(9, "销售报价")
    CRM_SALE_QUOTATION(9, "销售报价"),
    CRM_INVOICE(10, "开票")
    ;
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(CrmBizTypeEnum::getType).toArray(Integer[]::new);
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,139 @@
package cn.iocoder.yudao.module.crm.service.invoice;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoicePageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceSaveReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.invoice.CrmInvoiceDO;
import jakarta.validation.Valid;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
 * CRM å¼€ç¥¨ Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface CrmInvoiceService {
    /**
     * åˆ›å»ºå¼€ç¥¨
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return ç¼–号
     */
    Long createInvoice(@Valid CrmInvoiceSaveReqVO createReqVO);
    /**
     * æ›´æ–°å¼€ç¥¨
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateInvoice(@Valid CrmInvoiceSaveReqVO updateReqVO);
    /**
     * æ›´æ–°å¼€ç¥¨æµç¨‹å®¡æ‰¹ç»“æžœ
     *
     * @param id        å¼€ç¥¨ç¼–号
     * @param bpmResult BPM å®¡æ‰¹ç»“æžœ
     */
    void updateInvoiceAuditStatus(Long id, Integer bpmResult);
    /**
     * åˆ é™¤å¼€ç¥¨
     *
     * @param id ç¼–号
     */
    void deleteInvoice(Long id);
    /**
     * å‘起开票审批流程
     *
     * @param id     å¼€ç¥¨ç¼–号
     * @param userId ç”¨æˆ·ç¼–号
     */
    void submitInvoice(Long id, Long userId);
    /**
     * èŽ·å¾—å¼€ç¥¨
     *
     * @param id ç¼–号
     * @return å¼€ç¥¨
     */
    CrmInvoiceDO getInvoice(Long id);
    /**
     * èŽ·å¾—å¼€ç¥¨åˆ—è¡¨
     *
     * @param ids ç¼–号
     * @return å¼€ç¥¨åˆ—表
     */
    List<CrmInvoiceDO> getInvoiceList(Collection<Long> ids);
    /**
     * èŽ·å¾—å¼€ç¥¨ Map
     *
     * @param ids ç¼–号
     * @return å¼€ç¥¨ Map
     */
    default Map<Long, CrmInvoiceDO> getInvoiceMap(Collection<Long> ids) {
        return convertMap(getInvoiceList(ids), CrmInvoiceDO::getId);
    }
    /**
     * èŽ·å¾—å¼€ç¥¨åˆ†é¡µ
     *
     * æ•°æ®æƒé™ï¼šåŸºäºŽ {@link CrmInvoiceDO} è¯»å–
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @param userId    ç”¨æˆ·ç¼–号
     * @return å¼€ç¥¨åˆ†é¡µ
     */
    PageResult<CrmInvoiceDO> getInvoicePage(CrmInvoicePageReqVO pageReqVO, Long userId);
    /**
     * èŽ·å¾—å¼€ç¥¨åˆ†é¡µï¼ŒåŸºäºŽæŒ‡å®šå®¢æˆ·
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return å¼€ç¥¨åˆ†é¡µ
     */
    PageResult<CrmInvoiceDO> getInvoicePageByCustomerId(CrmInvoicePageReqVO pageReqVO);
    /**
     * èŽ·å¾—å¾…å®¡æ ¸å¼€ç¥¨æ•°é‡
     *
     * @param userId ç”¨æˆ·ç¼–号
     * @return å¾…审批数量
     */
    Long getAuditInvoiceCount(Long userId);
    /**
     * æ ¹æ®åˆåŒç¼–号查询开票列表
     *
     * @param contractId   åˆåŒç¼–号
     * @param auditStatuses å®¡æ‰¹çŠ¶æ€é›†åˆ
     * @return å¼€ç¥¨åˆ—表
     */
    List<CrmInvoiceDO> getInvoiceListByContractIdAndStatus(Long contractId, Collection<Integer> auditStatuses);
    /**
     * èŽ·å¾—åˆåŒå·²å¼€ç¥¨é‡‘é¢ Map(仅审批通过)
     *
     * @param contractIds åˆåŒç¼–号
     * @return å¼€ç¥¨é‡‘额 Map
     */
    Map<Long, BigDecimal> getInvoicePriceMapByContractId(Collection<Long> contractIds);
    /**
     * æ ¹æ®åˆåŒç¼–号查询开票数量
     *
     * @param contractId åˆåŒç¼–号
     * @return å¼€ç¥¨æ•°é‡
     */
    Long getInvoiceCountByContractId(Long contractId);
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,300 @@
package cn.iocoder.yudao.module.crm.service.invoice;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoicePageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceSaveReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.invoice.CrmInvoiceDO;
import cn.iocoder.yudao.module.crm.dal.mysql.invoice.CrmInvoiceMapper;
import cn.iocoder.yudao.module.crm.dal.redis.no.CrmNoRedisDAO;
import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.service.impl.DiffParseFunction;
import com.mzt.logapi.starter.annotation.LogRecord;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
import static cn.iocoder.yudao.module.crm.util.CrmAuditStatusUtils.convertBpmResultToAuditStatus;
/**
 * CRM å¼€ç¥¨ Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
@Slf4j
public class CrmInvoiceServiceImpl implements CrmInvoiceService {
    /**
     * BPM å¼€ç¥¨å®¡æ‰¹æµç¨‹æ ‡è¯†
     */
    public static final String BPM_PROCESS_DEFINITION_KEY = "crm-invoice-audit";
    /**
     * å‘票附件的业务记录类型,对应 {@link cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum#CRM_INVOICE}
     */
    private static final String INVOICE_RECORD_TYPE = "crm_invoice";
    @Resource
    private CrmInvoiceMapper invoiceMapper;
    @Resource
    private CrmNoRedisDAO noRedisDAO;
    @Resource
    private CrmContractService contractService;
    @Resource
    private CrmPermissionService permissionService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private BpmProcessInstanceApi bpmProcessInstanceApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_CREATE_SUB_TYPE, bizNo = "{{#invoice.id}}",
            success = CRM_INVOICE_CREATE_SUCCESS)
    public Long createInvoice(CrmInvoiceSaveReqVO createReqVO) {
        // 1.1 æ ¡éªŒå¯å¼€ç¥¨é‡‘额超过上限
        validateInvoicePriceExceedsLimit(createReqVO);
        // 1.2 æ ¡éªŒå…³è”数据存在
        validateRelationDataExists(createReqVO);
        // 1.3 ç”Ÿæˆå¼€ç¥¨ç¼–号
        String no = noRedisDAO.generate(CrmNoRedisDAO.INVOICE_PREFIX);
        if (invoiceMapper.selectByNo(no) != null) {
            throw exception(INVOICE_NO_EXISTS);
        }
        // 2.1 æ’入开票
        CrmInvoiceDO invoice = BeanUtils.toBean(createReqVO, CrmInvoiceDO.class)
                .setNo(no).setAuditStatus(CrmAuditStatusEnum.DRAFT.getStatus());
        invoiceMapper.insert(invoice);
        // 2.2 ç»‘定发票附件
        if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
            storageAttachmentApi.bindAttachments("file", INVOICE_RECORD_TYPE, invoice.getId(), createReqVO.getBlobIds());
        }
        // 3. åˆ›å»ºæ•°æ®æƒé™
        permissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_INVOICE.getType())
                .setBizId(invoice.getId()).setUserId(createReqVO.getOwnerUserId())
                .setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // è®¾ç½®å½“前操作的人为负责人
        // 4. è®°å½•操作日志上下文
        LogRecordContext.putVariable("invoice", invoice);
        return invoice.getId();
    }
    private void validateInvoicePriceExceedsLimit(CrmInvoiceSaveReqVO reqVO) {
        // 1. è®¡ç®—剩余可开票金额,不包括 reqVO è‡ªèº«
        CrmContractDO contract = contractService.validateContract(reqVO.getContractId());
        List<CrmInvoiceDO> invoices = invoiceMapper.selectListByContractIdAndStatus(reqVO.getContractId(),
                List.of(CrmAuditStatusEnum.APPROVE.getStatus()));
        if (reqVO.getId() != null) {
            invoices.removeIf(invoice -> ObjectUtil.equal(invoice.getId(), reqVO.getId()));
        }
        BigDecimal notInvoicePrice = contract.getTotalPrice().subtract(
                CollectionUtils.getSumValue(invoices, CrmInvoiceDO::getPrice, BigDecimal::add, BigDecimal.ZERO));
        // 2. æ ¡éªŒé‡‘额是否超过
        if (reqVO.getPrice().compareTo(notInvoicePrice) > 0) {
            throw exception(INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT, notInvoicePrice);
        }
    }
    private void validateRelationDataExists(CrmInvoiceSaveReqVO reqVO) {
        if (reqVO.getOwnerUserId() != null) {
            adminUserApi.validateUser(reqVO.getOwnerUserId()); // æ ¡éªŒè´Ÿè´£äººå­˜åœ¨
        }
        if (reqVO.getContractId() != null) {
            CrmContractDO contract = contractService.validateContract(reqVO.getContractId());
            if (ObjectUtil.notEqual(contract.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
                throw exception(INVOICE_CREATE_FAIL_CONTRACT_NOT_APPROVE);
            }
            reqVO.setCustomerId(contract.getCustomerId()); // è®¾ç½®å®¢æˆ·ç¼–号
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}",
            success = CRM_INVOICE_UPDATE_SUCCESS)
    @CrmPermission(bizType = CrmBizTypeEnum.CRM_INVOICE, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
    public void updateInvoice(CrmInvoiceSaveReqVO updateReqVO) {
        Assert.notNull(updateReqVO.getId(), "开票编号不能为空");
        updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null); // ä¸å…è®¸ä¿®æ”¹çš„字段
        // 1.1 æ ¡éªŒå­˜åœ¨
        CrmInvoiceDO oldInvoice = validateInvoiceExists(updateReqVO.getId());
        updateReqVO.setOwnerUserId(oldInvoice.getOwnerUserId()).setCustomerId(oldInvoice.getCustomerId())
                .setContractId(oldInvoice.getContractId()); // è®¾ç½®å·²å­˜åœ¨çš„值
        // 1.2 æ ¡éªŒå¯å¼€ç¥¨é‡‘额超过上限
        validateInvoicePriceExceedsLimit(updateReqVO);
        // 1.3 åªæœ‰è‰ç¨¿ã€å®¡æ‰¹ä¸­ï¼Œå¯ä»¥ç¼–辑;
        if (!ObjectUtils.equalsAny(oldInvoice.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
                CrmAuditStatusEnum.PROCESS.getStatus())) {
            throw exception(INVOICE_UPDATE_FAIL_EDITING_PROHIBITED);
        }
        // 2. æ›´æ–°å¼€ç¥¨
        CrmInvoiceDO updateObj = BeanUtils.toBean(updateReqVO, CrmInvoiceDO.class);
        invoiceMapper.updateById(updateObj);
        // 3. å¢žé‡æ›´æ–°å‘票附件
        storageAttachmentApi.updateAttachments("file", INVOICE_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
        // 4. è®°å½•操作日志上下文
        updateReqVO.setOwnerUserId(oldInvoice.getOwnerUserId()); // é¿å…æ“ä½œæ—¥å¿—出现“删除负责人”的情况
        LogRecordContext.putVariable("invoice", oldInvoice);
        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldInvoice, CrmInvoiceSaveReqVO.class));
    }
    @Override
    public void updateInvoiceAuditStatus(Long id, Integer bpmResult) {
        // 1.1 æ ¡éªŒå­˜åœ¨
        CrmInvoiceDO invoice = validateInvoiceExists(id);
        // 1.2 åªæœ‰å®¡æ‰¹ä¸­ï¼Œå¯ä»¥æ›´æ–°å®¡æ‰¹ç»“æžœ
        if (ObjUtil.notEqual(invoice.getAuditStatus(), CrmAuditStatusEnum.PROCESS.getStatus())) {
            log.error("[updateInvoiceAuditStatus][invoice({}) ä¸å¤„于审批中,无法更新审批结果({})]",
                    invoice.getId(), bpmResult);
            throw exception(INVOICE_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS);
        }
        // 2. æ›´æ–°å¼€ç¥¨å®¡æ‰¹çŠ¶æ€
        Integer auditStatus = convertBpmResultToAuditStatus(bpmResult);
        invoiceMapper.updateById(new CrmInvoiceDO().setId(id).setAuditStatus(auditStatus));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_DELETE_SUB_TYPE, bizNo = "{{#id}}",
            success = CRM_INVOICE_DELETE_SUCCESS)
    @CrmPermission(bizType = CrmBizTypeEnum.CRM_INVOICE, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
    public void deleteInvoice(Long id) {
        // 1.1 æ ¡éªŒå­˜åœ¨
        CrmInvoiceDO invoice = validateInvoiceExists(id);
        // 1.2 å®¡æ‰¹é€šè¿‡æ—¶ï¼Œä¸å…è®¸åˆ é™¤
        if (ObjUtil.equal(invoice.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
            throw exception(INVOICE_DELETE_FAIL_IS_APPROVE);
        }
        // 2.1 åˆ é™¤å‘票附件
        storageAttachmentApi.deleteAttachmentsByRecord(INVOICE_RECORD_TYPE, id);
        // 2.2 åˆ é™¤å¼€ç¥¨
        invoiceMapper.deleteById(id);
        // 2.3 åˆ é™¤æ•°æ®æƒé™
        permissionService.deletePermission(CrmBizTypeEnum.CRM_INVOICE.getType(), id);
        // 3. è®°å½•操作日志上下文
        LogRecordContext.putVariable("invoice", invoice);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_SUBMIT_SUB_TYPE, bizNo = "{{#id}}",
            success = CRM_INVOICE_SUBMIT_SUCCESS)
    public void submitInvoice(Long id, Long userId) {
        // 1. æ ¡éªŒå¼€ç¥¨æ˜¯å¦åœ¨å®¡æ‰¹
        CrmInvoiceDO invoice = validateInvoiceExists(id);
        if (ObjUtil.notEqual(invoice.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus())) {
            throw exception(INVOICE_SUBMIT_FAIL_NOT_DRAFT);
        }
        // 2. åˆ›å»ºå¼€ç¥¨å®¡æ‰¹æµç¨‹å®žä¾‹
        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
                .setProcessDefinitionKey(BPM_PROCESS_DEFINITION_KEY).setBusinessKey(String.valueOf(id)));
        // 3. æ›´æ–°å¼€ç¥¨å·¥ä½œæµç¼–号
        invoiceMapper.updateById(new CrmInvoiceDO().setId(id).setProcessInstanceId(processInstanceId)
                .setAuditStatus(CrmAuditStatusEnum.PROCESS.getStatus()));
        // 4. è®°å½•日志
        LogRecordContext.putVariable("invoiceNo", invoice.getNo());
    }
    private CrmInvoiceDO validateInvoiceExists(Long id) {
        CrmInvoiceDO invoice = invoiceMapper.selectById(id);
        if (invoice == null) {
            throw exception(INVOICE_NOT_EXISTS);
        }
        return invoice;
    }
    @Override
    @CrmPermission(bizType = CrmBizTypeEnum.CRM_INVOICE, bizId = "#id", level = CrmPermissionLevelEnum.READ)
    public CrmInvoiceDO getInvoice(Long id) {
        return invoiceMapper.selectById(id);
    }
    @Override
    public List<CrmInvoiceDO> getInvoiceList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return ListUtil.empty();
        }
        return invoiceMapper.selectByIds(ids);
    }
    @Override
    public PageResult<CrmInvoiceDO> getInvoicePage(CrmInvoicePageReqVO pageReqVO, Long userId) {
        return invoiceMapper.selectPage(pageReqVO, userId);
    }
    @Override
    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, bizId = "#pageReqVO.customerId", level = CrmPermissionLevelEnum.READ)
    public PageResult<CrmInvoiceDO> getInvoicePageByCustomerId(CrmInvoicePageReqVO pageReqVO) {
        return invoiceMapper.selectPageByCustomerId(pageReqVO);
    }
    @Override
    public Long getAuditInvoiceCount(Long userId) {
        return invoiceMapper.selectCountByAudit(userId);
    }
    @Override
    public List<CrmInvoiceDO> getInvoiceListByContractIdAndStatus(Long contractId, Collection<Integer> auditStatuses) {
        return invoiceMapper.selectListByContractIdAndStatus(contractId, auditStatuses);
    }
    @Override
    public Map<Long, BigDecimal> getInvoicePriceMapByContractId(Collection<Long> contractIds) {
        return invoiceMapper.selectInvoicePriceMapByContractId(contractIds);
    }
    @Override
    public Long getInvoiceCountByContractId(Long contractId) {
        return invoiceMapper.selectCountByContractId(contractId);
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/listener/CrmInvoiceStatusListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.crm.service.invoice.listener;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener;
import cn.iocoder.yudao.module.crm.service.invoice.CrmInvoiceService;
import cn.iocoder.yudao.module.crm.service.invoice.CrmInvoiceServiceImpl;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
/**
 * å¼€ç¥¨å®¡æ‰¹çš„结果的监听器实现类
 *
 * @author è¶…级管理员
 */
@Component
public class CrmInvoiceStatusListener extends BpmProcessInstanceStatusEventListener {
    @Resource
    private CrmInvoiceService invoiceService;
    @Override
    public String getProcessDefinitionKey() {
        return CrmInvoiceServiceImpl.BPM_PROCESS_DEFINITION_KEY;
    }
    @Override
    public void onEvent(BpmProcessInstanceStatusEvent event) {
        invoiceService.updateInvoiceAuditStatus(Long.parseLong(event.getBusinessKey()), event.getStatus());
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
@@ -14,6 +14,7 @@
import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivablePageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableSaveReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.invoice.CrmInvoiceDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivableMapper;
@@ -23,9 +24,12 @@
import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
import cn.iocoder.yudao.module.crm.service.invoice.CrmInvoiceService;
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.service.impl.DiffParseFunction;
import com.mzt.logapi.starter.annotation.LogRecord;
@@ -72,11 +76,15 @@
    private CrmReceivablePlanService receivablePlanService;
    @Resource
    private CrmPermissionService permissionService;
    @Resource
    private CrmInvoiceService invoiceService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private BpmProcessInstanceApi bpmProcessInstanceApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -85,9 +93,11 @@
    public Long createReceivable(CrmReceivableSaveReqVO createReqVO) {
        // 1.1 æ ¡éªŒå¯å›žæ¬¾é‡‘额超过上限
        validateReceivablePriceExceedsLimit(createReqVO);
        // 1.2 æ ¡éªŒå…³è”数据存在
        // 1.2 æ ¡éªŒå¯å›žæ¬¾é‡‘额不超过累计已开票金额(先开票后回款)
        validateReceivablePriceExceedsInvoiceLimit(createReqVO);
        // 1.3 æ ¡éªŒå…³è”数据存在
        validateRelationDataExists(createReqVO);
        // 1.3 ç”Ÿæˆå›žæ¬¾ç¼–号
        // 1.4 ç”Ÿæˆå›žæ¬¾ç¼–号
        String no = noRedisDAO.generate(CrmNoRedisDAO.RECEIVABLE_PREFIX);
        if (receivableMapper.selectByNo(no) != null) {
            throw exception(RECEIVABLE_NO_EXISTS);
@@ -131,6 +141,37 @@
        }
    }
    /**
     * æ ¡éªŒç´¯è®¡å›žæ¬¾é‡‘额不超过累计已开票且已上传发票附件的金额(先开票后回款)
     */
    private void validateReceivablePriceExceedsInvoiceLimit(CrmReceivableSaveReqVO reqVO) {
        // 1. è®¡ç®—合同累计回款金额(不含本次,统计口径同合同金额校验:审批通过 + å®¡æ‰¹ä¸­ï¼‰
        List<CrmReceivableDO> receivables = receivableMapper.selectListByContractIdAndStatus(reqVO.getContractId(),
                Arrays.asList(CrmAuditStatusEnum.APPROVE.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus()));
        if (reqVO.getId() != null) {
            receivables.removeIf(receivable -> ObjectUtil.equal(receivable.getId(), reqVO.getId()));
        }
        BigDecimal receivablePriceSum = CollectionUtils.getSumValue(receivables, CrmReceivableDO::getPrice,
                BigDecimal::add, BigDecimal.ZERO);
        // 2. è®¡ç®—合同下「审批通过且已上传发票附件」的累计开票金额
        List<CrmInvoiceDO> invoices = invoiceService.getInvoiceListByContractIdAndStatus(reqVO.getContractId(),
                List.of(CrmAuditStatusEnum.APPROVE.getStatus()));
        BigDecimal invoicePriceSum = BigDecimal.ZERO;
        for (CrmInvoiceDO invoice : invoices) {
            if (CollUtil.isNotEmpty(storageAttachmentApi.listAttachments(
                    StorageRecordTypeEnum.CRM_INVOICE.getType(), invoice.getId()))) {
                invoicePriceSum = invoicePriceSum.add(invoice.getPrice());
            }
        }
        // 3. æ ¡éªŒæœ¬æ¬¡å›žæ¬¾é‡‘额不超过剩余可回款金额(已开票金额 - å·²å›žæ¬¾é‡‘额)
        BigDecimal availablePrice = invoicePriceSum.subtract(receivablePriceSum);
        if (reqVO.getPrice().compareTo(availablePrice) > 0) {
            throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_NOT_ENOUGH, availablePrice);
        }
    }
    private void validateRelationDataExists(CrmReceivableSaveReqVO reqVO) {
        if (reqVO.getOwnerUserId() != null) {
            adminUserApi.validateUser(reqVO.getOwnerUserId()); // æ ¡éªŒè´Ÿè´£äººå­˜åœ¨
@@ -167,8 +208,10 @@
                .setContractId(oldReceivable.getContractId()).setPlanId(oldReceivable.getPlanId()); // è®¾ç½®å·²å­˜åœ¨çš„值
        // 1.2 æ ¡éªŒå¯å›žæ¬¾é‡‘额超过上限
        validateReceivablePriceExceedsLimit(updateReqVO);
        // 1.3 æ ¡éªŒå¯å›žæ¬¾é‡‘额不超过累计已开票金额(先开票后回款)
        validateReceivablePriceExceedsInvoiceLimit(updateReqVO);
        // 1.3 åªæœ‰è‰ç¨¿ã€å®¡æ‰¹ä¸­ï¼Œå¯ä»¥ç¼–辑;
        // 1.4 åªæœ‰è‰ç¨¿ã€å®¡æ‰¹ä¸­ï¼Œå¯ä»¥ç¼–辑;
        if (!ObjectUtils.equalsAny(oldReceivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
                CrmAuditStatusEnum.PROCESS.getStatus())) {
            throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED);
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
@@ -16,6 +16,11 @@
    ErrorCode EMPLOYEE_EDUCATION_NOT_EXISTS = new ErrorCode(1_030_000_003, "教育经历不存在");
    ErrorCode EMPLOYEE_WORK_HISTORY_NOT_EXISTS = new ErrorCode(1_030_000_004, "工作经历不存在");
    ErrorCode EMPLOYEE_EMERGENCY_CONTACT_NOT_EXISTS = new ErrorCode(1_030_000_005, "紧急联系人不存在");
    ErrorCode EMPLOYEE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_030_000_006, "导入员工数据不能为空");
    ErrorCode EMPLOYEE_DEPT_NAME_NOT_EXISTS = new ErrorCode(1_030_000_007, "部门【{}】不存在");
    ErrorCode EMPLOYEE_POST_NAME_NOT_EXISTS = new ErrorCode(1_030_000_008, "岗位【{}】不存在");
    ErrorCode EMPLOYEE_ROLE_NAME_NOT_EXISTS = new ErrorCode(1_030_000_009, "角色【{}】不存在");
    ErrorCode EMPLOYEE_IMPORT_FIELD_REQUIRED = new ErrorCode(1_030_000_010, "导入字段【{}】不能为空");
    // ========== è¯·å‡ç”³è¯· 1-030-001-000 ==========
    ErrorCode LEAVE_APPLICATION_NOT_EXISTS = new ErrorCode(1_030_001_000, "请假申请不存在");
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java
@@ -9,6 +9,9 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportExcelVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeePageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeSaveReqVO;
@@ -32,6 +35,7 @@
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
@@ -184,4 +188,29 @@
        ExcelUtils.write(response, "员工信息.xls", "数据", HrmEmployeeRespVO.class, list);
    }
    @GetMapping("/import-template")
    @Operation(summary = "下载员工导入模板")
    @PreAuthorize("@ss.hasPermission('hrm:employee:import')")
    public void importTemplate(HttpServletResponse response) throws IOException {
        // è¾“出示例数据,方便使用者参考
        List<HrmEmployeeImportExcelVO> list = Arrays.asList(
                HrmEmployeeImportExcelVO.builder().name("张三").gender(1).phone("13800138000")
                        .deptName("研发部").postName("部门经理").roleNames("普通管理员")
                        .hireDate(LocalDate.of(2020, 3, 1)).employeeStatus(1)
                        .idCard("320102199001150011").birthday(LocalDate.of(1990, 1, 15))
                        .nation("汉族").marriageStatus("已婚").age(36).education("本科").politicalStatus("中共党员")
                        .email("zhangsan@example.com").build()
        );
        ExcelUtils.write(response, "员工导入模板.xls", "员工列表", HrmEmployeeImportExcelVO.class, list);
    }
    @PostMapping("/import")
    @Operation(summary = "导入员工")
    @PreAuthorize("@ss.hasPermission('hrm:employee:import')")
    public CommonResult<HrmEmployeeImportRespVO> importExcel(@Valid HrmEmployeeImportReqVO importReqVO)
            throws Exception {
        List<HrmEmployeeImportExcelVO> list = ExcelUtils.read(importReqVO.getFile(), HrmEmployeeImportExcelVO.class);
        return success(employeeService.importEmployeeList(list, importReqVO));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeImportExcelVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo;
import cn.idev.excel.annotation.ExcelProperty;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
/**
 * å‘˜å·¥ Excel å¯¼å…¥ VO
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class HrmEmployeeImportExcelVO {
    @ExcelProperty("员工姓名")
    private String name;
    @ExcelProperty(value = "性别", converter = DictConvert.class)
    @DictFormat("hrm_gender")
    private Integer gender;
    @ExcelProperty("手机号")
    private String phone;
    @ExcelProperty("部门名称")
    private String deptName;
    @ExcelProperty("岗位名称")
    private String postName;
    @ExcelProperty("用户角色")
    private String roleNames;
    @ExcelProperty("入职日期")
    private LocalDate hireDate;
    @ExcelProperty(value = "员工状态", converter = DictConvert.class)
    @DictFormat("hrm_employee_status")
    private Integer employeeStatus;
    @ExcelProperty("身份证号")
    private String idCard;
    @ExcelProperty("出生日期")
    private LocalDate birthday;
    @ExcelProperty("民族")
    private String nation;
    @ExcelProperty("婚姻状态")
    private String marriageStatus;
    @ExcelProperty("年龄")
    private Integer age;
    @ExcelProperty("学历")
    private String education;
    @ExcelProperty("政治面貌")
    private String politicalStatus;
    @ExcelProperty("邮箱")
    private String email;
    @ExcelProperty("备注")
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeImportReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Schema(description = "管理后台 - å‘˜å·¥å¯¼å…¥ Request VO")
@Data
public class HrmEmployeeImportReqVO {
    @Schema(description = "Excel æ–‡ä»¶", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "Excel æ–‡ä»¶ä¸èƒ½ä¸ºç©º")
    private MultipartFile file;
    @Schema(description = "是否支持更新,默认 false", example = "true")
    private Boolean updateSupport = false;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeImportRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Schema(description = "管理后台 - å‘˜å·¥å¯¼å…¥ Response VO")
@Data
@Builder
public class HrmEmployeeImportRespVO {
    @Schema(description = "创建成功的员工名数组", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<String> createNames;
    @Schema(description = "更新成功的员工名数组", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<String> updateNames;
    @Schema(description = "导入失败的员工集合,key ä¸ºå‘˜å·¥åï¼Œvalue ä¸ºå¤±è´¥åŽŸå› ", requiredMode = Schema.RequiredMode.REQUIRED)
    private Map<String, String> failureNames;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeRespVO.java
@@ -64,6 +64,26 @@
    @ExcelProperty("身份证号")
    private String idCard;
    @Schema(description = "民族")
    @ExcelProperty("民族")
    private String nation;
    @Schema(description = "婚姻状态")
    @ExcelProperty("婚姻状态")
    private String marriageStatus;
    @Schema(description = "年龄")
    @ExcelProperty("年龄")
    private Integer age;
    @Schema(description = "学历")
    @ExcelProperty("学历")
    private String education;
    @Schema(description = "政治面貌")
    @ExcelProperty("政治面貌")
    private String politicalStatus;
    @Schema(description = "入职日期")
    @ExcelProperty("入职日期")
    private LocalDate hireDate;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeSaveReqVO.java
@@ -52,6 +52,21 @@
            message = "身份证号格式不正确")
    private String idCard;
    @Schema(description = "民族", example = "汉族")
    private String nation;
    @Schema(description = "婚姻状态", example = "已婚")
    private String marriageStatus;
    @Schema(description = "年龄", example = "30")
    private Integer age;
    @Schema(description = "学历", example = "本科")
    private String education;
    @Schema(description = "政治面貌", example = "中共党员")
    private String politicalStatus;
    @Schema(description = "入职日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2020-03-01")
    @NotNull(message = "入职日期不能为空")
    private LocalDate hireDate;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/HrmEmployeeDO.java
@@ -89,6 +89,31 @@
    private String idCard;
    /**
     * æ°‘族
     */
    private String nation;
    /**
     * å©šå§»çŠ¶æ€
     */
    private String marriageStatus;
    /**
     * å¹´é¾„
     */
    private Integer age;
    /**
     * å­¦åކ
     */
    private String education;
    /**
     * æ”¿æ²»é¢è²Œ
     */
    private String politicalStatus;
    /**
     * å…¥èŒæ—¥æœŸ
     */
    private LocalDate hireDate;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeService.java
@@ -1,6 +1,9 @@
package cn.iocoder.yudao.module.hrm.service.employee;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportExcelVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeePageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeSaveReqVO;
@@ -96,4 +99,14 @@
     */
    HrmEmployeeDO getEmployeeByUserId(Long userId);
    /**
     * å¯¼å…¥å‘˜å·¥åˆ—表
     *
     * @param importEmployees å‘˜å·¥å¯¼å…¥åˆ—表
     * @param importReqVO å¯¼å…¥è¯·æ±‚
     * @return å¯¼å…¥ç»“æžœ
     */
    HrmEmployeeImportRespVO importEmployeeList(List<HrmEmployeeImportExcelVO> importEmployees,
                                               HrmEmployeeImportReqVO importReqVO);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
@@ -3,10 +3,14 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeEducationSaveReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeEmergencyContactSaveReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportExcelVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeePageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeSaveReqVO;
@@ -30,12 +34,16 @@
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserCreateReqDTO;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -43,6 +51,7 @@
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*;
/**
@@ -72,6 +81,9 @@
    private AdminUserApi adminUserApi;
    @Resource
    private RoleApi roleApi;
    @Resource
    @Lazy // è‡ªæ³¨å…¥éœ€å»¶è¿ŸåŠ è½½ï¼Œå¦åˆ™ bean è¢«ä»£ç†åŒ…装后注入原始版本导致启动失败
    private HrmEmployeeService self;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -322,4 +334,131 @@
        return employeeMapper.selectByUserId(userId);
    }
    @Override
    public HrmEmployeeImportRespVO importEmployeeList(List<HrmEmployeeImportExcelVO> importEmployees,
                                                      HrmEmployeeImportReqVO importReqVO) {
        // 1. æ ¡éªŒéžç©º
        importEmployees = filterList(importEmployees, item -> Objects.nonNull(item.getName()));
        if (CollUtil.isEmpty(importEmployees)) {
            throw exception(EMPLOYEE_IMPORT_LIST_IS_EMPTY);
        }
        // 2. é¢„加载部门/岗位/角色 åç§° -> å®žä½“ æ˜ å°„
        Map<String, DeptRespDTO> deptMap = deptApi.getDeptList().stream()
                .collect(Collectors.toMap(DeptRespDTO::getName, dept -> dept, (v1, v2) -> v1));
        Map<String, PostRespDTO> postMap = postApi.getPostList().stream()
                .collect(Collectors.toMap(PostRespDTO::getName, post -> post, (v1, v2) -> v1));
        Map<String, RoleRespDTO> roleMap = roleApi.getRoleList().stream()
                .collect(Collectors.toMap(RoleRespDTO::getName, role -> role, (v1, v2) -> v1));
        // 3. é€æ¡å¤„理
        HrmEmployeeImportRespVO respVO = HrmEmployeeImportRespVO.builder()
                .createNames(new ArrayList<>())
                .updateNames(new ArrayList<>())
                .failureNames(new LinkedHashMap<>())
                .build();
        for (HrmEmployeeImportExcelVO importEmployee : importEmployees) {
            String name = importEmployee.getName();
            try {
                // 3.1 åç§°è½¬ ID
                Long deptId = parseDeptId(importEmployee.getDeptName(), deptMap);
                Long postId = parsePostId(importEmployee.getPostName(), postMap);
                Set<Long> roleIds = parseRoleIds(importEmployee.getRoleNames(), roleMap);
                // 3.2 æž„建保存请求
                HrmEmployeeSaveReqVO saveReqVO = buildImportSaveReqVO(importEmployee, deptId, postId, roleIds);
                // 3.3 åˆ¤æ–­å­˜åœ¨æ€§ï¼šæŒ‰æ‰‹æœºå·
                HrmEmployeeDO existEmployee = employeeMapper.selectByPhone(importEmployee.getPhone());
                if (existEmployee == null) {
                    self.createEmployee(saveReqVO);
                    respVO.getCreateNames().add(name);
                } else {
                    if (!Boolean.TRUE.equals(importReqVO.getUpdateSupport())) {
                        respVO.getFailureNames().put(name, EMPLOYEE_PHONE_EXISTS.getMsg());
                        continue;
                    }
                    saveReqVO.setId(existEmployee.getId());
                    self.updateEmployee(saveReqVO);
                    respVO.getUpdateNames().add(name);
                }
            } catch (ServiceException ex) {
                respVO.getFailureNames().put(name, ex.getMessage());
            } catch (Exception ex) {
                respVO.getFailureNames().put(name, ex.getMessage());
            }
        }
        return respVO;
    }
    private HrmEmployeeSaveReqVO buildImportSaveReqVO(HrmEmployeeImportExcelVO importEmployee,
                                                      Long deptId, Long postId, Set<Long> roleIds) {
        if (StrUtil.isBlank(importEmployee.getName())) {
            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "员工姓名");
        }
        if (StrUtil.isBlank(importEmployee.getPhone())) {
            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "手机号");
        }
        if (importEmployee.getHireDate() == null) {
            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "入职日期");
        }
        HrmEmployeeSaveReqVO reqVO = new HrmEmployeeSaveReqVO();
        reqVO.setName(importEmployee.getName());
        reqVO.setGender(importEmployee.getGender() != null ? importEmployee.getGender() : 1); // é»˜è®¤ç”·
        reqVO.setPhone(importEmployee.getPhone());
        reqVO.setDeptId(deptId);
        reqVO.setPostId(postId);
        reqVO.setRoleIds(CollUtil.isEmpty(roleIds) ? null : new ArrayList<>(roleIds));
        reqVO.setHireDate(importEmployee.getHireDate());
        reqVO.setEmployeeStatus(importEmployee.getEmployeeStatus() != null
                ? importEmployee.getEmployeeStatus() : 1); // é»˜è®¤åœ¨èŒ
        reqVO.setEmail(importEmployee.getEmail());
        reqVO.setBirthday(importEmployee.getBirthday());
        reqVO.setIdCard(importEmployee.getIdCard());
        reqVO.setNation(importEmployee.getNation());
        reqVO.setMarriageStatus(importEmployee.getMarriageStatus());
        reqVO.setAge(importEmployee.getAge());
        reqVO.setEducation(importEmployee.getEducation());
        reqVO.setPoliticalStatus(importEmployee.getPoliticalStatus());
        reqVO.setRemark(importEmployee.getRemark());
        return reqVO;
    }
    private Long parseDeptId(String deptName, Map<String, DeptRespDTO> deptMap) {
        if (StrUtil.isBlank(deptName)) {
            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "部门名称");
        }
        DeptRespDTO dept = deptMap.get(deptName.trim());
        if (dept == null) {
            throw exception(EMPLOYEE_DEPT_NAME_NOT_EXISTS, deptName.trim());
        }
        return dept.getId();
    }
    private Long parsePostId(String postName, Map<String, PostRespDTO> postMap) {
        if (StrUtil.isBlank(postName)) {
            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "岗位名称");
        }
        PostRespDTO post = postMap.get(postName.trim());
        if (post == null) {
            throw exception(EMPLOYEE_POST_NAME_NOT_EXISTS, postName.trim());
        }
        return post.getId();
    }
    private Set<Long> parseRoleIds(String roleNames, Map<String, RoleRespDTO> roleMap) {
        if (StrUtil.isBlank(roleNames)) {
            return Collections.emptySet();
        }
        Set<Long> roleIds = new LinkedHashSet<>();
        for (String roleName : roleNames.split("[,,]")) {
            String trimmed = roleName.trim();
            if (StrUtil.isBlank(trimmed)) {
                continue;
            }
            RoleRespDTO role = roleMap.get(trimmed);
            if (role == null) {
                throw exception(EMPLOYEE_ROLE_NAME_NOT_EXISTS, trimmed);
            }
            roleIds.add(role.getId());
        }
        return roleIds;
    }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/dept/DeptApi.java
@@ -31,6 +31,13 @@
    List<DeptRespDTO> getDeptList(Collection<Long> ids);
    /**
     * èŽ·å¾—å…¨éƒ¨éƒ¨é—¨åˆ—è¡¨
     *
     * @return éƒ¨é—¨ä¿¡æ¯æ•°ç»„
     */
    List<DeptRespDTO> getDeptList();
    /**
     * æ ¡éªŒéƒ¨é—¨ä»¬æ˜¯å¦æœ‰æ•ˆã€‚如下情况,视为无效:
     * 1. éƒ¨é—¨ç¼–号不存在
     * 2. éƒ¨é—¨è¢«ç¦ç”¨
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/dept/DeptApiImpl.java
@@ -2,6 +2,7 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
import cn.iocoder.yudao.module.system.service.dept.DeptService;
import org.springframework.stereotype.Service;
@@ -34,6 +35,12 @@
    }
    @Override
    public List<DeptRespDTO> getDeptList() {
        List<DeptDO> depts = deptService.getDeptList(new DeptListReqVO());
        return BeanUtils.toBean(depts, DeptRespDTO.class);
    }
    @Override
    public void validateDeptList(Collection<Long> ids) {
        deptService.validateDeptList(ids);
    }
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApi.java
@@ -27,6 +27,13 @@
    List<PostRespDTO> getPostList(Collection<Long> ids);
    /**
     * èŽ·å¾—å…¨éƒ¨å²—ä½åˆ—è¡¨
     *
     * @return å²—位信息数组
     */
    List<PostRespDTO> getPostList();
    default Map<Long, PostRespDTO> getPostMap(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return MapUtil.empty();
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApiImpl.java
@@ -32,4 +32,10 @@
        return BeanUtils.toBean(list, PostRespDTO.class);
    }
    @Override
    public List<PostRespDTO> getPostList() {
        List<PostDO> list = postService.getPostList(null, null);
        return BeanUtils.toBean(list, PostRespDTO.class);
    }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApi.java
@@ -40,6 +40,13 @@
    List<RoleRespDTO> getRoleList(Collection<Long> ids);
    /**
     * èŽ·å¾—å…¨éƒ¨è§’è‰²åˆ—è¡¨
     *
     * @return è§’色信息数组
     */
    List<RoleRespDTO> getRoleList();
    /**
     * èŽ·å¾—æŒ‡å®šç¼–å·çš„è§’è‰² Map
     *
     * @param ids è§’色编号数组
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApiImpl.java
@@ -37,4 +37,10 @@
        List<RoleDO> list = roleService.getRoleList(ids);
        return BeanUtils.toBean(list, RoleRespDTO.class);
    }
    @Override
    public List<RoleRespDTO> getRoleList() {
        List<RoleDO> list = roleService.getRoleList();
        return BeanUtils.toBean(list, RoleRespDTO.class);
    }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java
@@ -46,6 +46,7 @@
    // ===== CRM =====
    CRM_SALE_QUOTATION("crm_sale_quotation", "CRM é”€å”®æŠ¥ä»·å•"),
    CRM_FOLLOW_UP_RECORD("crm_follow_up_record", "CRM è·Ÿè¿›è®°å½•"),
    CRM_INVOICE("crm_invoice", "CRM å¼€ç¥¨"),
    // ===== HRM =====
    HRM_LEAVE_APPLICATION("hrm_leave_application", "HRM è¯·å‡ç”³è¯·"),
yudao-server/src/main/resources/application-jhhg.yaml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,324 @@
server:
  port: 48080
--- #################### æ•°æ®åº“相关配置 ####################
spring:
  autoconfigure:
    # noinspection SpringBootApplicationYaml
    exclude:
      - org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # é»˜è®¤ local çŽ¯å¢ƒï¼Œä¸å¼€å¯ Quartz çš„自动配置
      - org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Qdrant,手动创建
      - org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Redis,手动创建
      - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Milvus,手动创建
      - org.dromara.trans.config.TransServiceConfig # TODO èŠ‹è‰¿ï¼šeasy-trans å…¼å®¹ Spring Boot 4 åŽç§»é™¤ï¼ˆTransServiceConfig å¼•用了已移除的 RestTemplateBuilder)
      # ç¦ç”¨æœªé…ç½® API Key çš„æ¨¡åž‹è‡ªåŠ¨é…ç½®ï¼ˆåªä¿ç•™é€šä¹‰åƒé—® Embedding çš„æ‰‹åŠ¨é…ç½®ï¼‰
      - org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiAudioSpeechAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiAudioTranscriptionAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiModerationAutoConfiguration
      - org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration
      - org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration
      - org.springframework.ai.model.google.genai.autoconfigure.chat.GoogleGenAiChatAutoConfiguration
      - org.springframework.ai.model.google.genai.autoconfigure.embedding.GoogleGenAiEmbeddingConnectionAutoConfiguration
      - org.springframework.ai.model.google.genai.autoconfigure.embedding.GoogleGenAiTextEmbeddingAutoConfiguration
      - org.springframework.ai.model.ollama.autoconfigure.OllamaApiAutoConfiguration
      - org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration
      - org.springframework.ai.model.ollama.autoconfigure.OllamaEmbeddingAutoConfiguration
      - org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration
  # æ•°æ®æºé…ç½®é¡¹
  datasource:
    druid: # Druid ã€ç›‘控】相关的全局配置
      web-stat-filter:
        enabled: true
      stat-view-servlet:
        enabled: true
        allow: # è®¾ç½®ç™½åå•,不填则允许所有访问
        url-pattern: /druid/*
        login-username: # æŽ§åˆ¶å°ç®¡ç†ç”¨æˆ·åå’Œå¯†ç 
        login-password:
      filter:
        stat:
          enabled: true
          log-slow-sql: true # æ…¢ SQL è®°å½•
          slow-sql-millis: 100
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true
    dynamic: # å¤šæ•°æ®æºé…ç½®
      druid: # Druid ã€è¿žæŽ¥æ± ã€‘相关的全局配置
        initial-size: 1 # åˆå§‹è¿žæŽ¥æ•°
        min-idle: 1 # æœ€å°è¿žæŽ¥æ± æ•°é‡
        max-active: 20 # æœ€å¤§è¿žæŽ¥æ± æ•°é‡
        max-wait: 60000 # é…ç½®èŽ·å–è¿žæŽ¥ç­‰å¾…è¶…æ—¶çš„æ—¶é—´ï¼Œå•ä½ï¼šæ¯«ç§’ï¼ˆ1 åˆ†é’Ÿï¼‰
        time-between-eviction-runs-millis: 60000 # é…ç½®é—´éš”多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 åˆ†é’Ÿï¼‰
        min-evictable-idle-time-millis: 600000 # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å°ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位:毫秒(10 åˆ†é’Ÿï¼‰
        max-evictable-idle-time-millis: 1800000 # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å¤§ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位:毫秒(30 åˆ†é’Ÿï¼‰
        validation-query: SELECT 1 FROM DUAL # é…ç½®æ£€æµ‹è¿žæŽ¥æ˜¯å¦æœ‰æ•ˆ
        test-while-idle: true
        test-on-borrow: false
        test-on-return: false
        pool-prepared-statements: true # æ˜¯å¦å¼€å¯ PreparedStatement ç¼“å­˜
        max-pool-prepared-statement-per-connection-size: 20 # æ¯ä¸ªè¿žæŽ¥ç¼“存的 PreparedStatement æ•°é‡
      primary: master
      datasource:
        master:
          url: jdbc:mysql://172.17.0.1:9002/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
          #          url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X è¿žæŽ¥çš„示例
          #          url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL è¿žæŽ¥çš„示例
          #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle è¿žæŽ¥çš„示例
          #          url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true;useUnicode=true;characterEncoding=utf-8 # SQLServer è¿žæŽ¥çš„示例
          #          url: jdbc:dm://127.0.0.1:5236?schema=RUOYI_VUE_PRO # DM è¿žæŽ¥çš„示例
          #          url: jdbc:kingbase8://127.0.0.1:54321/test  # äººå¤§é‡‘仓 KingbaseES è¿žæŽ¥çš„示例
          #          url: jdbc:postgresql://127.0.0.1:5432/postgres # OpenGauss è¿žæŽ¥çš„示例
          #          url: jdbc:postgresql://127.0.0.1:5866/ruoyi-vue-pro # ç€šé«˜ HighGo è¿žæŽ¥çš„示例
          username: root
          password: jhhg@123456..
          #          username: sa # SQL Server è¿žæŽ¥çš„示例
          #          password: Yudao@2024 # SQL Server è¿žæŽ¥çš„示例
          #          username: SYSDBA # DM è¿žæŽ¥çš„示例
          #          password: SYSDBA001 # DM è¿žæŽ¥çš„示例
          #          username: root # OpenGauss è¿žæŽ¥çš„示例
          #          password: Yudao@2024 # OpenGauss è¿žæŽ¥çš„示例
          #          username: root # ç€šé«˜ HighGo è¿žæŽ¥çš„示例
          #          password: 123456 # ç€šé«˜ HighGo è¿žæŽ¥çš„示例
        slave: # æ¨¡æ‹Ÿä»Žåº“,可根据自己需要修改
          lazy: true # å¼€å¯æ‡’加载,保证启动速度
          url: jdbc:mysql://172.17.0.1:9002/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
          username: root
          password: jhhg@123456
#        tdengine: # IoT æ•°æ®åº“(需要 IoT ç‰©è”网再开启噢!)
#          lazy: true # å¼€å¯æ‡’加载,保证启动速度
#          url: jdbc:TAOS-WS://127.0.0.1:6041/ruoyi_vue_pro?varcharAsString=true&enableAutoReconnect=true # å‚见:https://t.zsxq.com/qaX9c,开启 TDengine WebSocket è‡ªåŠ¨é‡è¿ž
#          driver-class-name: com.taosdata.jdbc.ws.WebSocketDriver
#          username: root
#          password: taosdata
#          druid:
#            validation-query: SELECT SERVER_STATUS() # TDengine æ•°æ®æºçš„æœ‰æ•ˆæ€§æ£€æŸ¥ SQL
  # Redis é…ç½®ã€‚Redisson é»˜è®¤çš„配置足够使用,一般不需要进行调优
  data:
    redis:
      host: 172.17.0.1 # åœ°å€
      port: 6379 # ç«¯å£
      database: 1 # æ•°æ®åº“索引
#      password: dev # å¯†ç ï¼Œå»ºè®®ç”Ÿäº§çŽ¯å¢ƒå¼€å¯
--- #################### å®šæ—¶ä»»åŠ¡ç›¸å…³é…ç½® ####################
# Quartz é…ç½®é¡¹ï¼Œå¯¹åº” QuartzProperties é…ç½®ç±»
spring:
  quartz:
    auto-startup: true # æœ¬åœ°å¼€å‘环境,尽量不要开启 Job
    scheduler-name: schedulerName # Scheduler åå­—。默认为 schedulerName
    job-store-type: jdbc # Job å­˜å‚¨å™¨ç±»åž‹ã€‚默认为 memory è¡¨ç¤ºå†…存,可选 jdbc ä½¿ç”¨æ•°æ®åº“。
    wait-for-jobs-to-complete-on-shutdown: true # åº”用关闭时,是否等待定时任务执行完成。默认为 false ï¼Œå»ºè®®è®¾ç½®ä¸º true
    properties: # æ·»åŠ  Quartz Scheduler é™„加属性,更多可以看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html æ–‡æ¡£
      org:
        quartz:
          # Scheduler ç›¸å…³é…ç½®
          scheduler:
            instanceName: schedulerName
            instanceId: AUTO # è‡ªåŠ¨ç”Ÿæˆ instance ID
          # JobStore ç›¸å…³é…ç½®
          jobStore:
            # JobStore å®žçŽ°ç±»ã€‚å¯è§åšå®¢ï¼šhttps://blog.csdn.net/weixin_42458219/article/details/122247162
            class: org.springframework.scheduling.quartz.LocalDataSourceJobStore
            isClustered: true # æ˜¯é›†ç¾¤æ¨¡å¼
            clusterCheckinInterval: 15000 # é›†ç¾¤æ£€æŸ¥é¢‘率,单位:毫秒。默认为 15000,即 15 ç§’
            misfireThreshold: 60000 # misfire é˜€å€¼ï¼Œå•位:毫秒。
          # çº¿ç¨‹æ± ç›¸å…³é…ç½®
          threadPool:
            threadCount: 25 # çº¿ç¨‹æ± å¤§å°ã€‚默认为 10 ã€‚
            threadPriority: 5 # çº¿ç¨‹ä¼˜å…ˆçº§
            class: org.quartz.simpl.SimpleThreadPool # çº¿ç¨‹æ± ç±»åž‹
    jdbc: # ä½¿ç”¨ JDBC çš„ JobStore çš„æ—¶å€™ï¼ŒJDBC çš„配置
      initialize-schema: NEVER # æ˜¯å¦è‡ªåŠ¨ä½¿ç”¨ SQL åˆå§‹åŒ– Quartz è¡¨ç»“构。这里设置成 never ï¼Œæˆ‘们手动创建表结构。
--- #################### æ¶ˆæ¯é˜Ÿåˆ—相关 ####################
# rocketmq é…ç½®é¡¹ï¼Œå¯¹åº” RocketMQProperties é…ç½®ç±»
rocketmq:
  name-server: 127.0.0.1:9876 # RocketMQ Namesrv
spring:
  # RabbitMQ é…ç½®é¡¹ï¼Œå¯¹åº” RabbitProperties é…ç½®ç±»
  rabbitmq:
    host: 127.0.0.1 # RabbitMQ æœåŠ¡çš„åœ°å€
    port: 5672 # RabbitMQ æœåŠ¡çš„ç«¯å£
    username: rabbit # RabbitMQ æœåŠ¡çš„è´¦å·
    password: rabbit # RabbitMQ æœåŠ¡çš„å¯†ç 
  # Kafka é…ç½®é¡¹ï¼Œå¯¹åº” KafkaProperties é…ç½®ç±»
  kafka:
    bootstrap-servers: 127.0.0.1:9092 # æŒ‡å®š Kafka Broker åœ°å€ï¼Œå¯ä»¥è®¾ç½®å¤šä¸ªï¼Œä»¥é€—号分隔
--- #################### æœåŠ¡ä¿éšœç›¸å…³é…ç½® ####################
# Lock4j é…ç½®é¡¹
lock4j:
  acquire-timeout: 3000 # èŽ·å–åˆ†å¸ƒå¼é”è¶…æ—¶æ—¶é—´ï¼Œé»˜è®¤ä¸º 3000 æ¯«ç§’
  expire: 30000 # åˆ†å¸ƒå¼é”çš„超时时间,默认为 30000 æ¯«ç§’
--- #################### ç›‘控相关配置 ####################
# Actuator ç›‘控端点的配置项
management:
  endpoints:
    web:
      base-path: /actuator # Actuator æä¾›çš„ API æŽ¥å£çš„æ ¹ç›®å½•。默认为 /actuator
      exposure:
        include: '*' # éœ€è¦å¼€æ”¾çš„端点。默认值只打开 health å’Œ info ä¸¤ä¸ªç«¯ç‚¹ã€‚通过设置 * ï¼Œå¯ä»¥å¼€æ”¾æ‰€æœ‰ç«¯ç‚¹ã€‚
# Spring Boot Admin é…ç½®é¡¹
spring:
  boot:
    admin:
      # Spring Boot Admin Client å®¢æˆ·ç«¯çš„相关配置
      client:
        url: http://127.0.0.1:${server.port}/${spring.boot.admin.context-path} # è®¾ç½® Spring Boot Admin Server åœ°å€
        instance:
          service-host-type: IP # æ³¨å†Œå®žä¾‹æ—¶ï¼Œä¼˜å…ˆä½¿ç”¨ IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
        username: admin
        password: admin
      # Spring Boot Admin Server æœåŠ¡ç«¯çš„ç›¸å…³é…ç½®
      context-path: /admin # é…ç½® Spring
      # å…è®¸åµŒå…¥ iframe çš„域名(支持通配符),实际部署时,可以改为 "'self' [你的公网域名]"
      frame-ancestors: "'self' localhost localhost:48080 127.0.0.1 127.0.0.1:48080"
# æ—¥å¿—文件配置
logging:
  file:
    name: logs/${spring.application.name}.log # æ—¥å¿—文件名,相对于项目根目录
  level:
    # é…ç½®è‡ªå·±å†™çš„ MyBatis Mapper æ‰“印日志
    cn.iocoder.yudao.module.bpm.dal.mysql: debug
    cn.iocoder.yudao.module.infra.dal.mysql: debug
    cn.iocoder.yudao.module.infra.dal.mysql.logger.ApiErrorLogMapper: INFO # é…ç½® ApiErrorLogMapper çš„æ—¥å¿—级别为 info,避免和 GlobalExceptionHandler é‡å¤æ‰“印
    cn.iocoder.yudao.module.infra.dal.mysql.job.JobLogMapper: INFO # é…ç½® JobLogMapper çš„æ—¥å¿—级别为 info
    cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper: INFO # é…ç½® FileConfigMapper çš„æ—¥å¿—级别为 info
    cn.iocoder.yudao.module.pay.dal.mysql: debug
    cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyTaskMapper: INFO # é…ç½® PayNotifyTaskMapper çš„æ—¥å¿—级别为 info
    cn.iocoder.yudao.module.system.dal.mysql: debug
    cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper: INFO # é…ç½® SmsChannelMapper çš„æ—¥å¿—级别为 info
    cn.iocoder.yudao.module.tool.dal.mysql: debug
    cn.iocoder.yudao.module.member.dal.mysql: debug
    cn.iocoder.yudao.module.trade.dal.mysql: debug
    cn.iocoder.yudao.module.promotion.dal.mysql: debug
    cn.iocoder.yudao.module.statistics.dal.mysql: debug
    cn.iocoder.yudao.module.crm.dal.mysql: debug
    cn.iocoder.yudao.module.erp.dal.mysql: debug
    cn.iocoder.yudao.module.mes.dal.mysql: debug
    cn.iocoder.yudao.module.wms.dal.mysql: debug
    cn.iocoder.yudao.module.iot.dal.mysql: debug
    cn.iocoder.yudao.module.iot.dal.tdengine: DEBUG
    cn.iocoder.yudao.module.iot.service.rule: debug
    cn.iocoder.yudao.module.ai.dal.mysql: debug
    cn.iocoder.yudao.module.im.dal.mysql: debug
    org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO èŠ‹è‰¿ï¼šå…ˆç¦ç”¨ï¼ŒSpring Boot 3.X å­˜åœ¨éƒ¨åˆ†é”™è¯¯çš„ WARN æç¤º
debug: false
yudao:
  ai:
    vector-store:
      type: milvus # å‘量库类型:milvus
      milvus:
        initialize-schema: true
        database-name: test
        collection-name: knowledge_segment_jhhg
        host: 36.213.180.217
        port: 19530
--- #################### å¾®ä¿¡å…¬ä¼—号、小程序相关配置 ####################
wx:
  mp: # å…¬ä¼—号配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md æ–‡æ¡£
#    app-id: wx041349c6f39b268b # æµ‹è¯•号(牛希尧提供的)
#    secret: 5abee519483bc9f8cb37ce280e814bd0
#    app-id: wx5b23ba7a5589ecbb # æµ‹è¯•号(自己的)
#    secret: 2a7b3b20c537e52e74afd395eb85f61f
    app-id: wxf56b1542b9e85f8a # æµ‹è¯•号(Kongdy æä¾›çš„)
    secret: 496379dcef1ba869e9234de8d598cfd3
    # å­˜å‚¨é…ç½®ï¼Œè§£å†³ AccessToken çš„跨节点的共享
    config-storage:
      type: RedisTemplate # é‡‡ç”¨ RedisTemplate æ“ä½œ Redis,会自动从 Spring ä¸­èŽ·å–
      key-prefix: wx # Redis Key çš„前缀
      http-client-type: HttpComponents # é‡‡ç”¨ HttpComponents(HttpClient 5)请求微信公众号平台
  miniapp: # å°ç¨‹åºé…ç½®ï¼ˆå¿…填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md æ–‡æ¡£
    #    appid: wx62056c0d5e8db250 # æµ‹è¯•号(牛希尧提供的)
    #    secret: 333ae72f41552af1e998fe1f54e1584a
#    appid: wx63c280fe3248a3e7 # wenhualian的接口测试号
#    secret: 6f270509224a7ae1296bbf1c8cb97aed
    appid: wxc4598c446f8a9cb3 # æµ‹è¯•号(Kongdy æä¾›çš„)
    secret: 4a1a04e07f6a4a0751b39c3064a92c8b
#    appid: wx66186af0759f47c9 # æµ‹è¯•号(puhui æä¾›çš„)
#    secret: 3218bcbd112cbc614c7264ceb20144ac
    config-storage:
      type: RedisTemplate # é‡‡ç”¨ RedisTemplate æ“ä½œ Redis,会自动从 Spring ä¸­èŽ·å–
      key-prefix: wa # Redis Key çš„前缀
      http-client-type: HttpComponents # é‡‡ç”¨ HttpComponents(HttpClient 5)请求微信小程序平台
--- #################### è¶…级管理员相关配置 ####################
# è¶…级管理员配置项,设置当前项目所有自定义的配置
yudao:
  captcha:
    enable: false # æœ¬åœ°çŽ¯å¢ƒï¼Œæš‚æ—¶å…³é—­å›¾ç‰‡éªŒè¯ç ï¼Œæ–¹ä¾¿ç™»å½•ç­‰æŽ¥å£çš„æµ‹è¯•ï¼›
  security:
    mock-enable: true
  pay:
    order-notify-url: https://yutou.mynatapp.cc/admin-api/pay/notify/order # æ”¯ä»˜æ¸ é“的【支付】回调地址
    refund-notify-url: https://yutou.mynatapp.cc/admin-api/pay/notify/refund # æ”¯ä»˜æ¸ é“的【退款】回调地址
    transfer-notify-url: https://yutou.mynatapp.cc/admin-api/pay/notify/transfer # æ”¯ä»˜æ¸ é“的【转账】回调地址
  access-log: # è®¿é—®æ—¥å¿—的配置项
    enable: false
  demo: false # å…³é—­æ¼”示模式
  wxa-code:
    env-version: develop # å°ç¨‹åºç‰ˆæœ¬: æ­£å¼ç‰ˆä¸º "release";体验版为 "trial";开发版为 "develop"
  wxa-subscribe-message:
    miniprogram-state: developer # è·³è½¬å°ç¨‹åºç±»åž‹ï¼šå¼€å‘版为 â€œdeveloper”;体验版为 â€œtrial”为;正式版为 â€œformal”
  tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ åœ°å›¾çš„密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
  storage: # æ–‡ä»¶ä¸Šä¼ é…ç½®
    path: /data/javaWork/mom/file # æ–‡ä»¶å­˜å‚¨æ ¹ç›®å½•
    url-prefix: /admin-api/system/storage-blob # URL路径前缀
    domain: http://42.63.70.90:9003 # åŸŸåå‰ç¼€ï¼ˆæŒ‰å®žé™…域名配置),前端配置好像不太对现在是直连后端,所有要后端端口
    expired: 120 # token过期时间(分钟)
    use-limit: 10 # token使用次数上限
    jwt-secret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG # JWT密钥
    compress: true # æ˜¯å¦åŽ‹ç¼©å›¾ç‰‡
    need-compress-size: 10MB # è§¦å‘压缩的文件大小阈值
    compress-quality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
justauth:
  enabled: true
  type:
    DINGTALK: # é’‰é’‰
      client-id: dingvrnreaje3yqvzhxg
      client-secret: i8E6iZyDvZj51JIb0tYsYfVQYOks9Cq1lgryEjFRqC79P3iJcrxEwT6Qk2QvLrLI
      ignore-check-redirect-uri: true
    WECHAT_ENTERPRISE: # ä¼ä¸šå¾®ä¿¡
      client-id: wwd411c69a39ad2e54
      client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw
      agent-id: 1000004
      ignore-check-redirect-uri: true
    # noinspection SpringBootApplicationYaml
    WECHAT_MINI_PROGRAM: # å¾®ä¿¡å°ç¨‹åº
      client-id: ${wx.miniapp.appid}
      client-secret: ${wx.miniapp.secret}
      ignore-check-redirect-uri: true
      ignore-check-state: true # å¾®ä¿¡å°ç¨‹åºï¼Œä¸ä¼šä½¿ç”¨åˆ° state,所以不进行校验
    WECHAT_MP: # å¾®ä¿¡å…¬ä¼—号
      client-id: ${wx.mp.app-id}
      client-secret: ${wx.mp.secret}
      ignore-check-redirect-uri: true
    ALIPAY: # æ”¯ä»˜å®å°ç¨‹åº
      client-id: xx
      client-secret: xx
      alipay-public-key: xx
      ignore-check-redirect-uri: true
      ignore-check-state: true
  cache:
    type: REDIS
    prefix: 'social_auth_state:' # ç¼“存前缀,目前只对 Redis ç¼“存生效,默认 JUSTAUTH::STATE::
    timeout: 24h # è¶…时时长,目前只对 Redis ç¼“存生效,默认 3 åˆ†é’Ÿ