已添加7个文件
已修改6个文件
1017 ■■■■■ 文件已修改
docs/process_feedback_wait_countdown_integration.md 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/process_feedback_wait_countdown_remind_integration.md 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/product_receipt_auto_stock_integration.md 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/database/DatabaseBackupController.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupServiceImpl.java 460 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-dev.yaml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-jhhg.yaml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-local.yaml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-test.yaml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-wtxc.yaml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-xjln.yaml 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-yzfx.yaml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/process_feedback_wait_countdown_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,116 @@
# æŠ¥å·¥ç­‰å¾…时长倒计时 + æŠ¥å·¥æ•°é‡å¯é€‰ - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- å·¥åºç®¡ç†ï¼ˆ`src/views/mes/pro/process`,表单 `modules/form.vue`)
- å¿«é€ŸæŠ¥å·¥ï¼ˆ`src/views/mes/pro/workorder-center/modules/quick-feedback.vue`)
- æŠ¥å·¥è¡¨å•(`src/views/mes/pro/feedback/modules/form.vue`)
- ç”Ÿäº§ä»»åŠ¡ç›¸å…³å±•ç¤ºï¼ˆåˆ—è¡¨/详情,报工倒计时状态展示)
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. **工序主数据配置**:在工序管理页面为工序配置「等待时长(小时)」与「是否需要填写报工数量」两个新字段。字段保存到工序主数据(`mes_pro_process`),所有引用该工序的工艺路线共用。
2. **报工界面读取工序配置**:报工表单(新增/编辑)选择任务后,通过 `getRouteProcessByRouteAndProcess(routeId, processId)`(现有调用)拉取工序配置,响应新增返回 `waitDuration`、`needReportQuantity`。
   - `waitDuration > 0`:该工序报工前必须先点击「开始倒计时」,倒计时结束才能报工。
   - `needReportQuantity = false`:报工数量允许为空(不显示数量输入项、不做"必须大于 0"校验)。
3. **开始倒计时**:操作员点击「开始倒计时」→ è°ƒç”¨æ–°æŽ¥å£ `startWaitCountdown` â†’ åŽç«¯æŠŠå½“前时间写入任务(`mes_pro_task.wait_start_time`)。
4. **任务状态回显**:任务列表/详情响应新增 `waitStartTime` å­—段,前端用于展示倒计时状态(已开始/进行中/已结束)。
5. **提交报工**:报工提交(快速报工 / æŠ¥å·¥è¡¨å•提交 / å®¡æ‰¹é€šè¿‡ï¼‰æ—¶ï¼ŒåŽç«¯ç»Ÿä¸€æ ¡éªŒç­‰å¾…时长与数量规则。
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/mes/pro/task/start-wait-countdown` | å¼€å§‹æŠ¥å·¥å€’计时(记录开始时间到任务) |
| GET | `/mes/pro/route/process/get-by-route-and-process` | çŽ°æœ‰æŽ¥å£ï¼Œå“åº”æ–°å¢ž `waitDuration`/`needReportQuantity` |
### æ–°å¢žæŽ¥å£ï¼šå¼€å§‹æŠ¥å·¥å€’计时
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| taskId | Long | æ˜¯ | ç”Ÿäº§ä»»åŠ¡ç¼–å·ï¼ˆPOST body JSON ä¼ é€’) |
**请求示例:**
```json
{
  "taskId": 35
}
```
**响应:** è¿”回倒计时开始时间(`yyyy-MM-dd HH:mm:ss`)。
**响应示例:**
```json
{
  "code": 0,
  "data": "2026-08-29 10:30:00"
}
```
**业务前置校验(后端):**
| åœºæ™¯ | ç»“æžœ |
|------|------|
| ä»»åŠ¡ä¸å­˜åœ¨ / å·²ç»ˆæ€ï¼ˆå®Œæˆã€å–消) | æŠ¥é”™ï¼ˆä»»åŠ¡å¼‚å¸¸ï¼‰ |
| ä»»åŠ¡å·²åœå·¥ | æŠ¥é”™ï¼ˆåœå·¥ä¸èƒ½æŠ¥å·¥ï¼‰ |
| å·¥åºæœªé…ç½®ç­‰å¾…时长(为空或 0) | æŠ¥é”™ã€Œè¯¥å·¥åºæœªé…ç½®ç­‰å¾…时长,无需倒计时」 |
| å·¥åºå·²é…ç½®ç­‰å¾…æ—¶é•¿ | æˆåŠŸï¼Œè®°å½•å¼€å§‹æ—¶é—´ï¼›é‡å¤ç‚¹å‡»ä¼šé‡ç½®ä¸ºå½“å‰æ—¶é—´ |
### çŽ°æœ‰æŽ¥å£å“åº”å­—æ®µæ–°å¢ž
**`get-by-route-and-process` å“åº”新增字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| waitDuration | BigDecimal | ç­‰å¾…时长(小时)。>0 è¡¨ç¤ºæŠ¥å·¥å‰éœ€å€’计时等待 |
| needReportQuantity | Boolean | æ˜¯å¦éœ€è¦å¡«å†™æŠ¥å·¥æ•°é‡ã€‚`false`=报工数量允许为空 |
**生产任务相关响应新增字段(列表/详情/任务选择组件):**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| waitStartTime | LocalDateTime | æŠ¥å·¥å€’计时开始时间。为空表示尚未开始倒计时 |
**工序主数据保存/响应新增字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| waitDuration | BigDecimal | ç­‰å¾…时长(小时),报工前需倒计时等待 |
| needReportQuantity | Boolean | æ˜¯å¦éœ€è¦å¡«å†™æŠ¥å·¥æ•°é‡ |
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| ç­‰å¾…时长(小时) | å·¥åºç®¡ç†è¡¨å• | æ•°å­—输入,小数最多 2 ä½ï¼›ç•™ç©º/0 è¡¨ç¤ºè¯¥å·¥åºæŠ¥å·¥æ— éœ€å€’计时 |
| æ˜¯å¦éœ€è¦å¡«å†™æŠ¥å·¥æ•°é‡ | å·¥åºç®¡ç†è¡¨å• | å¼€å…³ã€‚关闭后该工序报工数量允许为空 |
| å¾…报工数量 | å¿«é€ŸæŠ¥å·¥ / æŠ¥å·¥è¡¨å• | çŽ°æœ‰å­—æ®µã€‚è‹¥å·¥åºæ— éœ€å¡«å†™æ•°é‡ï¼ŒæŠ¥å·¥æ•°é‡è¾“å…¥é¡¹å¯ä¸æ˜¾ç¤º |
| æœ¬æ¬¡æŠ¥å·¥æ•°é‡ | å¿«é€ŸæŠ¥å·¥ / æŠ¥å·¥è¡¨å• | æŒ‰å·¥åºé…ç½®å†³å®šæ˜¯å¦å¿…å¡«/可空 |
| å¼€å§‹å€’计时 æŒ‰é’® + å€’计时剩余时间 | å¿«é€ŸæŠ¥å·¥ / æŠ¥å·¥è¡¨å• | å·¥åºé…ç½®ç­‰å¾…时长时展示;点击后展示倒计时剩余时间 |
| å€’计时状态 | ç”Ÿäº§ä»»åŠ¡åˆ—è¡¨/详情(可选) | ä¾æ® `waitStartTime` ä¸Žå½“前时间计算剩余时间 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| å·¥åº `waitDuration` ä¸ºç©ºæˆ– 0 | æŠ¥å·¥æ— éœ€å€’计时,原流程不变 |
| å·¥åº `waitDuration > 0`,任务未点「开始倒计时」 | æŠ¥å·¥æŠ¥é”™ã€Œè¯¥å·¥åºè®¾ç½®äº†ç­‰å¾…时长,请先点击"开始倒计时"」 |
| å·¥åº `waitDuration > 0`,倒计时未结束 | æŠ¥å·¥æŠ¥é”™ã€Œç­‰å¾…倒计时未结束,还需等待 X小时X分钟」 |
| å·¥åº `waitDuration > 0`,倒计时已结束 | å¯æ­£å¸¸æŠ¥å·¥ï¼›å¼€å§‹æ—¶é—´è®°å½•在任务上,每任务一次 |
| é‡å¤ç‚¹å‡»ã€Œå¼€å§‹å€’计时」 | é‡ç½®å¼€å§‹æ—¶é—´ä¸ºå½“前时间 |
| å·¥åº `needReportQuantity = true` æˆ–未配置 | æŠ¥å·¥æ•°é‡å¿…é¡» > 0(原逻辑) |
| å·¥åº `needReportQuantity = false` | æŠ¥å·¥æ•°é‡å…è®¸ä¸ºç©º/0;后端跳过数量校验,审批时不生成产出单/批次,不扣倒冲物料,仅完成报工 |
| å€’计时适用范围 | æŒ‰å·¥åºåˆ¤æ–­ï¼Œå¼€å§‹æ—¶é—´è®°å½•到生产任务,每任务一次 |
## æ³¨æ„äº‹é¡¹
- ç­‰å¾…时长单位固定为**小时**,前端输入时注意单位提示(示例 `2.00` è¡¨ç¤º 2 å°æ—¶ï¼›`0.5` è¡¨ç¤º 30 åˆ†é’Ÿï¼‰ã€‚
- å€’计时开始时间记录在**生产任务**上,与工序等待时长(主数据)分开维护;修改工序等待时长不影响已开始的任务倒计时。
- è‹¥å·¥åºé…ç½®äº†ç­‰å¾…时长但任务未开始倒计时,直接提交报工会被后端拦截(创建、快速报工、审批通过三处入口都校验)。
- å¿«é€ŸæŠ¥å·¥å½“前为数量必填,接入后需按 `needReportQuantity` è°ƒæ•´ã€Œè¯·è¾“入报工数量」的必填校验与数量输入框展示。
- æŠ¥å·¥æ•°é‡ä¸ºç©º/0 çš„æŠ¥å·¥ï¼šä¸ä¼šç”Ÿæˆäº§å‡ºå•/批次、不会倒冲扣料,仅把报工标记为已完成并更新任务进度;请在前端相应隐藏数量相关展示,避免误导。
- æŠ¥å·¥è¡¨å•编辑/审批时任务可能已过等待期,此时无需再倒计时即可提交。
docs/process_feedback_wait_countdown_remind_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
# æŠ¥å·¥å€’计时系统消息提醒(10分钟 / 5分钟 / ç»“束) - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- æ— å‰ç«¯é¡µé¢æ”¹åŠ¨ã€‚æé†’ç”±**后端定时任务**自动发送到**系统站内信(消息中心)**,操作员在站内信列表查看即可。
## è¯´æ˜Ž
报工倒计时设置系统消息提醒,分三个等级自动发送站内信:
| ç­‰çº§ | è§¦å‘时机 | ç«™å†…信模板 code |
|------|----------|-----------------|
| 1 | å€’计时剩余 â‰¤ 10 åˆ†é’Ÿ | `mes_task_wait_remind_10` |
| 2 | å€’计时剩余 â‰¤ 5 åˆ†é’Ÿ | `mes_task_wait_remind_5` |
| 3 | å€’计时结束(剩余 â‰¤ 0) | `mes_task_wait_remind_end` |
后端新增定时任务,**每分钟扫描一次**所有已开始倒计时(`waitStartTime` éžç©ºï¼‰ä¸”未终态、未停工的报工任务,到达对应时间点自动发送站内信。
## ç«™å†…信内容
| æ¨¡æ¿ code | å†…容示例 |
|-----------|----------|
| `mes_task_wait_remind_10` | ç”Ÿäº§ä»»åŠ¡ã€{taskCode}】{taskName} æŠ¥å·¥å€’计时剩余 10 åˆ†é’Ÿï¼Œè¯·å‡†å¤‡æŠ¥å·¥ã€‚ |
| `mes_task_wait_remind_5` | ç”Ÿäº§ä»»åŠ¡ã€{taskCode}】{taskName} æŠ¥å·¥å€’计时剩余 5 åˆ†é’Ÿï¼Œå³å°†å¯ä»¥æŠ¥å·¥ã€‚ |
| `mes_task_wait_remind_end` | ç”Ÿäº§ä»»åŠ¡ã€{taskCode}】{taskName} æŠ¥å·¥å€’计时已结束,可以进行报工。 |
占位符:`taskCode`(任务编码)、`taskName`(任务名称)、`processName`(工序名称)。
## æŽ¥æ”¶äºº
1. **优先**:任务指定的报工人员(`mes_pro_task_assignee`),全部发送。
2. **兜底**:任务未指定报工人员时,发给任务创建人。
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| å·¥åºç­‰å¾…时长不足 10 åˆ†é’Ÿï¼ˆå¦‚ 5 åˆ†é’Ÿï¼‰ | å¼€å§‹å€’计时后立即触发"10 åˆ†é’Ÿ"与"5 åˆ†é’Ÿ"提醒(剩余不足即提醒) |
| é‡å¤ç‚¹å‡»"开始倒计时"重置时间 | æé†’按新的开始时间重新计算,重新触发一轮提醒 |
| ä»»åŠ¡å·²å®Œæˆ / å·²å–消 / å·²åœå·¥ | ä¸å†æ‰«ææé†’ |
| åŒä¸€ç­‰çº§åŒä¸€è½®å€’计时 | åªæé†’一次(Redis åŽ»é‡ï¼Œ24 å°æ—¶ï¼‰ |
## æ³¨æ„äº‹é¡¹
- æé†’为**系统站内信**,操作员在系统消息中心查看,无弹窗/无前端接口调用。
- ç«™å†…信模板可在「系统管理 â†’ ç«™å†…信模板」中按 code æŸ¥çœ‹/修改内容。
- è¯¥åŠŸèƒ½ä¸ºæ–°å¢žï¼Œä¸å½±å“åŽŸæœ‰æŠ¥å·¥å€’è®¡æ—¶æŠ¥å·¥æ ¡éªŒé€»è¾‘ã€‚
docs/product_receipt_auto_stock_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,111 @@
# æŠ¥å·¥è‡ªåŠ¨ç”Ÿæˆäº§å“å…¥åº“å• + å‹¾é€‰å…¥åº“ + ä¸åˆæ ¼ä»“库兜底 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- äº§å“å…¥åº“(`src/views/wls/productreceipt`,列表 `index.vue` + è¡¨å• `modules/form.vue`、行 `modules/line-list.vue`、上架明细 `modules/detail-form.vue`)
- æŠ¥å·¥å®¡æ‰¹ï¼ˆä»…数据来源,无需改动,审批通过后自动生成入库单)
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. **报工审批 â†’ è‡ªåŠ¨ç”Ÿæˆå…¥åº“å•**:报工审批通过后,后端自动生成一条产品入库单(`mes_wm_product_receipt`),一次报工=一条入库单(幂等)。
2. **行 = äº§å‡ºè¡Œ**:入库单行来自生产入库单产出行(`mes_wm_product_produce_line`),按合格/不合格分行,每行自动带入 `itemId`、`quantity`(产出数量)、`batchId/batchCode`、`qualityStatus`(质量状态合格/不合格),默认 `isStock=false`(未勾选入库)。
3. **一个数量一行**:产品入库页表格按"一行=一个数量"展示,行即本次报工的一个产出数量(合格行/不合格行各一条)。
4. **执行入库前勾选**:在「待上架」状态的入库单上,每行勾选"是否入库";勾选入库的行展开三级(仓库→库区→库位)多选并分配数量。
5. **执行上架保存**:调用新接口 `submit-stock` ä¿å­˜æ¯è¡Œçš„勾选状态与仓库/库区/库位分配。
6. **执行入库兜底**:执行入库时,未勾选入库的行或分配数量不足的行,剩余数量自动进「不合格仓库」(三级:DEFECT_WAREHOUSE / DEFECT_LOCATION / DEFECT_AREA),每个数量生成一条流水(出虚拟线边库 + å…¥å®žé™…/不合格仓库)。
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/mes/wm/product-receipt/submit-stock` | æ‰¹é‡ä¸Šæž¶ï¼ˆå‹¾é€‰æ˜¯å¦å…¥åº“ + ä»“库/库区/库位分配数量),新增 |
| PUT | `/mes/wm/product-receipt/submit` | æäº¤ï¼ˆè‰ç¨¿ â†’ å¾…上架),现有 |
| PUT | `/mes/wm/product-receipt/stock` | æ‰§è¡Œä¸Šæž¶ï¼ˆå¾…上架 â†’ å¾…执行入库),现有 |
| PUT | `/mes/wm/product-receipt/finish` | æ‰§è¡Œå…¥åº“(待执行入库 â†’ å·²å®Œæˆï¼‰ï¼ŒçŽ°æœ‰ |
### æ–°å¢žæŽ¥å£ï¼šæ‰¹é‡ä¸Šæž¶
**请求参数(`submit-stock`):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| receiptId | Long | æ˜¯ | äº§å“å…¥åº“单编号 |
| rows | List\<Row\> | æ˜¯ | è¡Œä¸Šæž¶åˆ—表 |
| rows[].lineId | Long | æ˜¯ | å…¥åº“单行编号 |
| rows[].isStock | Boolean | æ˜¯ | æ˜¯å¦å…¥åº“(true=勾选入库并分配仓库/库位;false=执行入库时自动进不合格仓库) |
| rows[].details | List\<Detail\> | æ¡ä»¶ | isStock=true æ—¶å¿…须;每个分配一条,数量之和=行收货数量 |
| details[].warehouseId | Long | æ˜¯ | ä»“库编号 |
| details[].locationId | Long | æ˜¯ | åº“区编号 |
| details[].areaId | Long | æ˜¯ | åº“位编号 |
| details[].quantity | BigDecimal | æ˜¯ | åˆ†é…æ•°é‡ |
**请求示例:**
```json
{
  "receiptId": 1,
  "rows": [
    {
      "lineId": 11,
      "isStock": true,
      "details": [
        { "warehouseId": 1, "locationId": 1, "areaId": 1, "quantity": 60 },
        { "warehouseId": 1, "locationId": 2, "areaId": 2, "quantity": 40 }
      ]
    },
    {
      "lineId": 12,
      "isStock": false
    }
  ]
}
```
**响应:** `{ "code": 0, "data": true }`
### çŽ°æœ‰æŽ¥å£å“åº”å­—æ®µæ–°å¢ž
**产品入库单详情/分页响应(RespVO)新增:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| feedbackId | Long | æ¥æºæŠ¥å·¥ç¼–号。非空表示由报工自动生成,不可手工改行 |
**产品入库单行响应(LineRespVO)新增:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| qualityStatus | Integer | è´¨é‡çŠ¶æ€ã€‚å­—å…¸ `MES_WM_QUALITY_STATUS`(合格/不合格),自动生成的入库单行带入 |
| isStock | Boolean | æ˜¯å¦å…¥åº“(false/空=执行入库时默认进不合格仓库) |
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| æŠ¥å·¥æ¥æº | åˆ—表 | æœ‰ `feedbackId` çš„入库单可展示"自动生成"标识 |
| è´¨é‡çŠ¶æ€ | è¡Œè¡¨æ ¼ | å±•示合格/不合格,来自产出 |
| æ˜¯å¦å…¥åº“ | è¡Œè¡¨æ ¼ | æ¯è¡Œä¸€ä¸ªå‹¾é€‰ï¼ˆä»…待上架状态可改);勾选后展开仓库/库区/库位分配 |
| ä»“库/库区/库位 | è¡Œå±•开明细 | ä¸‰çº§çº§è”选择,可多选;每条明细一个数量 |
| åˆ†é…æ•°é‡ | è¡Œå±•开明细 | å‹¾é€‰å…¥åº“的行要求分配数量之和 = è¡Œæ”¶è´§æ•°é‡ |
| å‰©ä½™æ•°é‡ | è¡Œå±•开明细 | è¡Œæ”¶è´§æ•°é‡ - å·²åˆ†é…æ•°é‡ï¼›æ‰§è¡Œå…¥åº“时剩余自动进不合格仓库 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æŠ¥å·¥å®¡æ‰¹é€šè¿‡ï¼ˆæ— è´¨æ£€ï¼‰ | è‡ªåŠ¨ç”Ÿæˆå…¥åº“å•ï¼Œè¡Œ=合格/不合格产出行,状态=草稿 |
| æŠ¥å·¥å®¡æ‰¹é€šè¿‡ï¼ˆè´¨æ£€ï¼ŒIPQC å®ŒæˆåŽï¼‰ | è‡ªåŠ¨ç”Ÿæˆå…¥åº“å•ï¼Œè¡Œ=合格/不合格拆分行 |
| é‡å¤è§¦å‘同一报工 | å¹‚等:已存在则不再生成第二条 |
| isStock=false æˆ–未分配完 | æ‰§è¡Œå…¥åº“时剩余数量自动进不合格仓库 |
| isStock=true ä½†åˆ†é…æ•°é‡ä¹‹å’Œ â‰  è¡Œæ”¶è´§æ•°é‡ | æŠ¥é”™ã€Œå‹¾é€‰å…¥åº“的分配数量与行收货数量不一致」 |
| ä¸åˆæ ¼ä»“库未配置 | æ‰§è¡Œå…¥åº“报错「未配置不合格仓库,请先创建 DEFECT_WAREHOUSE ä»“库」 |
| æ‰§è¡Œå…¥åº“ | æ¯æ¡æ˜Žç»†ç”Ÿæˆä¸€æ¡æµæ°´ï¼ˆå‡ºè™šæ‹Ÿçº¿è¾¹åº“ + å…¥å®žé™…/不合格仓库),一个数量=一条流水 |
| è‡ªåŠ¨ç”Ÿæˆçš„å…¥åº“å• | è¡Œä¸å¯æ‰‹å·¥å¢žåˆ æ”¹ï¼ˆå¯æ•´å•取消);手工创建的入库单不受影响 |
## æ³¨æ„äº‹é¡¹
- å¾…上架状态才允许勾选"是否入库"与分配仓库/库位;执行上架保存后状态变为待执行入库。
- å‹¾é€‰å…¥åº“的行必须分配完整数量;只分配一部分时,剩余部分执行入库仍会进不合格仓库。
- ä¸åˆæ ¼æ•°é‡é»˜è®¤ä¸å‹¾é€‰å…¥åº“(进不合格仓库);合格数量由操作员决定是否入库及入哪个仓库。
- è‡ªåŠ¨ç”Ÿæˆçš„å…¥åº“å• `qualityStatus` ç”±äº§å‡ºè¡Œå¸¦å…¥ï¼Œè¡Œæœ¬èº«ä¸å¯ä¿®æ”¹ï¼›å¦‚需调整入库去向,通过勾选与分配实现。
- å…¥åº“流水以"数量"为单位:同一行分到不同仓库/库位,一条明细对应一条 OUT(虚拟线边库)+ IN(实际仓库)流水。
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/database/DatabaseBackupController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.system.controller.admin.database;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.system.service.database.DatabaseBackupService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
@Tag(name = "管理后台 - æ•°æ®åº“备份")
@RestController
@RequestMapping("/system/database")
public class DatabaseBackupController {
    @Resource
    private DatabaseBackupService databaseBackupService;
    @GetMapping("/backup")
    @Operation(summary = "下载数据库备份")
    @PreAuthorize("@ss.hasPermission('system:database:backup')")
    public void backupDatabase(HttpServletResponse response) throws Exception {
        databaseBackupService.backupDatabase(response);
    }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package cn.iocoder.yudao.module.system.service.database;
import jakarta.servlet.http.HttpServletResponse;
public interface DatabaseBackupService {
    /**
     * å¤‡ä»½æ•°æ®åº“,并将 SQL æ–‡ä»¶å†™å…¥åˆ° HTTP å“åº”中
     *
     * @param response HTTP å“åº”
     */
    void backupDatabase(HttpServletResponse response) throws Exception;
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,460 @@
package cn.iocoder.yudao.module.system.service.database;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.sql.DataSource;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
@Slf4j
public class DatabaseBackupServiceImpl implements DatabaseBackupService {
    /** å¤‡ä»½è¶…时时间(分钟) */
    private static final long BACKUP_TIMEOUT_MINUTES = 10;
    /** MySQL å®¹å™¨å†…部端口 */
    private static final String DOCKER_MYSQL_PORT = "3306";
    @Value("${spring.datasource.dynamic.datasource.master.url:}")
    private String dbUrl;
    @Value("${spring.datasource.dynamic.datasource.master.username:root}")
    private String dbUsername;
    @Value("${spring.datasource.dynamic.datasource.master.password:}")
    private String dbPassword;
    /** å¤‡ä»½å‘½ä»¤ï¼Œå¯æ˜¾å¼æŒ‡å®šï¼ˆå¦‚ docker exec mysql8 mysqldump);留空则自动检测本地 mysqldump æˆ– Docker MySQL å®¹å™¨ */
    @Value("${yudao.database-backup.command:}")
    private String backupCommand;
    /** å¤‡ä»½ç›®æ ‡æ•°æ®åº“ host,留空则从数据源 JDBC URL è§£æž */
    @Value("${yudao.database-backup.host:}")
    private String backupHost;
    /** å¤‡ä»½ç›®æ ‡æ•°æ®åº“ port,留空则从数据源 JDBC URL è§£æž */
    @Value("${yudao.database-backup.port:}")
    private String backupPort;
    /** å¤‡ä»½ç›®æ ‡æ•°æ®åº“名,留空则从数据源 JDBC URL è§£æž */
    @Value("${yudao.database-backup.database:}")
    private String backupDatabase;
    /** å¤‡ä»½è´¦å·ï¼Œç•™ç©ºåˆ™ä½¿ç”¨æ•°æ®æºè´¦å· */
    @Value("${yudao.database-backup.username:}")
    private String backupUsername;
    /** å¤‡ä»½å¯†ç ï¼Œç•™ç©ºåˆ™ä½¿ç”¨æ•°æ®æºå¯†ç  */
    @Value("${yudao.database-backup.password:}")
    private String backupPassword;
    /** ä¸»æ•°æ®æºï¼Œç”¨äºŽ JDBC å¯¼å‡ºå…œåº• */
    @Resource
    private DataSource dataSource;
    @Override
    public void backupDatabase(HttpServletResponse response) throws Exception {
        if (StrUtil.isEmpty(dbUrl)) {
            throw new ServiceException(500, "未找到数据库连接配置");
        }
        // å¤‡ä»½è¿žæŽ¥çš„æ•°æ®åº“信息:优先使用 yudao.database-backup é…ç½®ï¼Œæœªé…ç½®çš„部分从数据源 JDBC URL è§£æžå…œåº•
        String dbHost = backupHost;
        String dbPort = backupPort;
        String dbName = backupDatabase;
        if (StrUtil.isEmpty(dbHost) || StrUtil.isEmpty(dbPort) || StrUtil.isEmpty(dbName)) {
            Matcher matcher = Pattern.compile("jdbc:mysql://([^:]+):(\\d+)/([^?]+)").matcher(dbUrl);
            if (matcher.find()) {
                if (StrUtil.isEmpty(dbHost)) {
                    dbHost = matcher.group(1);
                }
                if (StrUtil.isEmpty(dbPort)) {
                    dbPort = matcher.group(2);
                }
                if (StrUtil.isEmpty(dbName)) {
                    dbName = matcher.group(3);
                }
            }
        }
        if (StrUtil.isEmpty(dbHost) || StrUtil.isEmpty(dbPort) || StrUtil.isEmpty(dbName)) {
            log.warn("无法从配置或 JDBC URL è§£æžå®Œæ•´çš„æ•°æ®åº“信息: host={}, port={}, db={}", dbHost, dbPort, dbName);
            throw new ServiceException(500, "无法解析数据库备份连接信息,请在 yudao.database-backup ä¸­é…ç½® host/port/database");
        }
        // localhost ç»Ÿä¸€èµ° TCP 127.0.0.1:mysqldump å¯¹ localhost å¯èƒ½èµ° socket/loopback,
        // è€Œä¸åŒçŽ¯å¢ƒ root@localhost çš„认证要求差异大,强制 TCP æ›´ç¨³å®š
        if ("localhost".equalsIgnoreCase(dbHost)) {
            dbHost = "127.0.0.1";
        }
        String username = StrUtil.isEmpty(backupUsername) ? dbUsername : backupUsername;
        String password = StrUtil.isEmpty(backupPassword) ? dbPassword : backupPassword;
        // ç»„装候选备份命令,按优先级依次尝试:手动配置 > æœ¬æœº mysqldump > Docker å®¹å™¨ mysqldump
        List<List<String>> candidates = new ArrayList<>();
        List<String> labels = new ArrayList<>();
        if (StrUtil.isNotEmpty(backupCommand)) {
            candidates.add(splitCommand(backupCommand));
            labels.add("手动配置命令[" + backupCommand + "]");
        }
        if (isLocalMysqldumpAvailable()) {
            candidates.add(buildLocalMysqldumpCommand(dbHost, dbPort, username, password, dbName));
            labels.add("本机 mysqldump");
        }
        String container = findMysqlDockerContainer(dbPort);
        if (StrUtil.isNotEmpty(container)) {
            candidates.add(buildDockerExecCommand(container, username, password, dbName));
            labels.add("Docker å®¹å™¨[" + container + "] mysqldump");
        }
        log.info("数据库备份候选方式:{}", labels);
        // ä¾æ¬¡å°è¯•所有外部命令方式
        List<String> failures = new ArrayList<>();
        String timestamp = DateUtil.format(new Date(), "yyyyMMdd_HHmmss");
        String fileName = "backup_" + timestamp + ".sql";
        for (int i = 0; i < candidates.size(); i++) {
            File tempFile = File.createTempFile("backup_", ".sql");
            try {
                String error = runMysqldump(candidates.get(i), password, tempFile);
                if (error == null) {
                    log.info("数据库备份成功,使用方式:{}", labels.get(i));
                    downloadAttachment(response, tempFile, fileName);
                    return;
                }
                log.warn("备份方式 [{}] å¤±è´¥ï¼š{}", labels.get(i), error);
                failures.add(labels.get(i) + ":" + error);
            } finally {
                FileUtil.del(tempFile);
            }
        }
        // å¤–部命令全部失败或不可用时,使用 JDBC å¯¼å‡ºå…œåº•,保证任何部署环境(如应用容器内无 mysqldump/docker)都能备份
        File tempFile = File.createTempFile("backup_", ".sql");
        try {
            String error = exportByJdbc(tempFile);
            if (error == null) {
                log.info("数据库备份成功,使用方式:JDBC å¯¼å‡ºå…œåº•");
                downloadAttachment(response, tempFile, fileName);
                return;
            }
            log.warn("JDBC å¯¼å‡ºå…œåº•失败:{}", error);
            failures.add("JDBC å¯¼å‡ºå…œåº•:" + error);
        } finally {
            FileUtil.del(tempFile);
        }
        throw new ServiceException(500, "数据库备份失败:" + String.join(" ï½œ ", failures));
    }
    /**
     * é€šè¿‡ JDBC ç›´è¿žæ•°æ®åº“导出建表语句与全量数据,作为外部命令不可用时的兜底备份方式
     *
     * @return æ‰§è¡ŒæˆåŠŸè¿”å›ž null;失败返回错误描述
     */
    private String exportByJdbc(File outputFile) {
        try (Connection conn = dataSource.getConnection();
             BufferedWriter writer = new BufferedWriter(
                     new OutputStreamWriter(new FileOutputStream(outputFile), StandardCharsets.UTF_8))) {
            writer.write("/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n");
            writer.write("/*!40101 SET NAMES utf8mb4 */;\n");
            writer.write("SET TIME_ZONE='+00:00';\n");
            writer.write("SET FOREIGN_KEY_CHECKS=0;\n");
            writer.write("SET UNIQUE_CHECKS=0;\n");
            writer.write("SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO';\n\n");
            try (Statement st = conn.createStatement();
                 ResultSet rs = st.executeQuery("SHOW TABLES")) {
                List<String> tables = new ArrayList<>();
                while (rs.next()) {
                    tables.add(rs.getString(1));
                }
                for (String table : tables) {
                    exportTable(conn, writer, table);
                }
            }
            writer.write("\nSET FOREIGN_KEY_CHECKS=1;\n");
            writer.write("SET UNIQUE_CHECKS=1;\n");
            writer.write("/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n");
            return null;
        } catch (Exception e) {
            log.error("JDBC å¯¼å‡ºå…œåº•失败", e);
            return "JDBC å¯¼å‡ºå¼‚常: " + e.getMessage();
        }
    }
    /**
     * å¯¼å‡ºå•张表的建表语句与全部数据
     */
    private void exportTable(Connection conn, BufferedWriter writer, String table) throws Exception {
        String quoted = "`" + table.replace("`", "``") + "`";
        // å»ºè¡¨è¯­å¥
        try (Statement st = conn.createStatement();
             ResultSet rs = st.executeQuery("SHOW CREATE TABLE " + quoted)) {
            if (rs.next()) {
                writer.write(rs.getString(2));
                writer.write(";\n\n");
            }
        }
        // å…¨é‡æ•°æ®ï¼Œæµå¼è¯»å–避免大表占用过多内存
        try (Statement st = conn.createStatement()) {
            st.setFetchSize(Integer.MIN_VALUE);
            try (ResultSet rs = st.executeQuery("SELECT * FROM " + quoted)) {
                ResultSetMetaData meta = rs.getMetaData();
                int columnCount = meta.getColumnCount();
                int[] types = new int[columnCount];
                for (int i = 0; i < columnCount; i++) {
                    types[i] = meta.getColumnType(i + 1);
                }
                while (rs.next()) {
                    StringBuilder sb = new StringBuilder("INSERT INTO ").append(quoted).append(" VALUES (");
                    for (int i = 0; i < columnCount; i++) {
                        if (i > 0) {
                            sb.append(',');
                        }
                        if (rs.getObject(i + 1) == null) {
                            sb.append("NULL");
                            continue;
                        }
                        int type = types[i];
                        if (type == Types.BIT) {
                            sb.append(rs.getBoolean(i + 1) ? '1' : '0');
                        } else if (isBinaryType(type)) {
                            sb.append("X'").append(HexUtil.encodeHexStr(rs.getBytes(i + 1))).append("'");
                        } else {
                            sb.append('\'').append(escapeSql(rs.getString(i + 1))).append('\'');
                        }
                    }
                    sb.append(");\n");
                    writer.write(sb.toString());
                }
            }
        }
        writer.write('\n');
    }
    /**
     * æ˜¯å¦äºŒè¿›åˆ¶åˆ—类型,二进制数据以十六进制字面量导出避免乱码
     */
    private boolean isBinaryType(int type) {
        return type == Types.BINARY || type == Types.VARBINARY
                || type == Types.LONGVARBINARY || type == Types.BLOB;
    }
    /**
     * SQL å­—符串字面量转义
     */
    private String escapeSql(String value) {
        return value.replace("\\", "\\\\").replace("'", "''").replace("\0", "\\0");
    }
    /**
     * æž„建本机 mysqldump å¤‡ä»½å‘½ä»¤
     */
    private List<String> buildLocalMysqldumpCommand(String host, String port, String username, String password, String dbName) {
        List<String> command = new ArrayList<>();
        command.add("mysqldump");
        // MySQL 8 caching_sha2_password è®¤è¯é»˜è®¤ä¸è¯·æ±‚服务端公钥,会报 Access denied (using password: YES)
        command.add("--get-server-public-key");
        command.add("-h" + host);
        command.add("-P" + port);
        command.add("-u" + username);
        if (StrUtil.isEmpty(password)) {
            command.add("--skip-password");
        }
        command.add(dbName);
        return command;
    }
    /**
     * æž„建通过 docker exec è°ƒç”¨å®¹å™¨å†… mysqldump çš„备份命令
     */
    private List<String> buildDockerExecCommand(String container, String username, String password, String dbName) {
        List<String> command = new ArrayList<>();
        command.add("docker");
        command.add("exec");
        // docker exec ä¸ä¼šè½¬å‘宿主机环境变量,密码需通过 -e æ˜¾å¼ä¼ å…¥å®¹å™¨
        if (StrUtil.isNotEmpty(password)) {
            command.add("-e");
            command.add("MYSQL_PWD=" + password);
        }
        command.add(container);
        command.add("mysqldump");
        command.add("--get-server-public-key");
        // å®¹å™¨å†…固定走 127.0.0.1:3306 çš„ TCP è¿žæŽ¥è‡ªèº«ï¼Œç»•å¼€ socket/loopback è®¤è¯å·®å¼‚
        command.add("-h127.0.0.1");
        command.add("-P" + DOCKER_MYSQL_PORT);
        command.add("-u" + username);
        if (StrUtil.isEmpty(password)) {
            command.add("--skip-password");
        }
        command.add(dbName);
        return command;
    }
    /**
     * æ‰§è¡Œ mysqldump å‘½ä»¤ï¼Œå°† stdout å†™å…¥ä¸´æ—¶æ–‡ä»¶
     *
     * @return æ‰§è¡ŒæˆåŠŸè¿”å›ž null;失败返回错误描述
     */
    private String runMysqldump(List<String> command, String password, File outputFile) {
        try {
            ProcessBuilder pb = new ProcessBuilder(command);
            // å¯†ç é€šè¿‡çŽ¯å¢ƒå˜é‡ä¼ é€’ï¼Œé¿å…å‡ºçŽ°åœ¨å‘½ä»¤è¡ŒåŠè¿›ç¨‹åˆ—è¡¨
            if (StrUtil.isNotEmpty(password)) {
                pb.environment().put("MYSQL_PWD", password);
            }
            Process process = pb.start();
            // åŽå°è¯»å– stderr,防止管道缓冲区写满导致进程阻塞
            StringBuilder stderr = new StringBuilder();
            Thread stderrThread = new Thread(() -> {
                try (BufferedReader reader = new BufferedReader(
                        new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
                    String line;
                    while ((line = reader.readLine()) != null) {
                        stderr.append(line).append(System.lineSeparator());
                    }
                } catch (IOException ignored) {
                }
            });
            stderrThread.start();
            // mysqldump è¾“出写入本地临时文件(docker exec åœºæ™¯è¾“出不会落在容器内文件系统)
            try (InputStream in = process.getInputStream();
                 OutputStream out = new FileOutputStream(outputFile)) {
                IoUtil.copy(in, out);
            }
            boolean finished = process.waitFor(BACKUP_TIMEOUT_MINUTES, TimeUnit.MINUTES);
            stderrThread.join();
            if (!finished) {
                process.destroyForcibly();
                return "执行超时(超过 " + BACKUP_TIMEOUT_MINUTES + " åˆ†é’Ÿï¼‰";
            }
            if (process.exitValue() != 0) {
                return "exitCode=" + process.exitValue() + "," + stderr.toString().trim();
            }
            return null;
        } catch (IOException e) {
            return "启动命令失败: " + e.getMessage();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "执行被中断: " + e.getMessage();
        }
    }
    /**
     * å°†å¤‡ä»½æ–‡ä»¶ä½œä¸ºé™„件输出到响应
     */
    private void downloadAttachment(HttpServletResponse response, File file, String fileName) throws IOException {
        response.setContentType("application/octet-stream; charset=UTF-8");
        response.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(fileName, "UTF-8") + "\"");
        response.setContentLengthLong(file.length());
        try (FileInputStream fis = new FileInputStream(file);
             OutputStream os = response.getOutputStream()) {
            IoUtil.copy(fis, os);
        }
    }
    /**
     * å°†é…ç½®çš„命令字符串按空白切分为参数列表
     */
    private List<String> splitCommand(String command) {
        List<String> parts = new ArrayList<>();
        for (String token : command.trim().split("\\s+")) {
            parts.add(token);
        }
        return parts;
    }
    /**
     * æ£€æµ‹æœ¬æœº mysqldump æ˜¯å¦å¯ç”¨
     */
    private boolean isLocalMysqldumpAvailable() {
        try {
            Process process = new ProcessBuilder("mysqldump", "--version").redirectErrorStream(true).start();
            boolean finished = process.waitFor(5, TimeUnit.SECONDS);
            if (!finished) {
                process.destroyForcibly();
                return false;
            }
            return process.exitValue() == 0;
        } catch (IOException e) {
            return false;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
    /**
     * æŸ¥æ‰¾è¿è¡Œä¸­çš„ MySQL Docker å®¹å™¨ï¼Œä¼˜å…ˆé€‰æ‹©ç«¯å£æ˜ å°„与数据源端口一致的容器
     *
     * @return å®¹å™¨åï¼›æœªæ‰¾åˆ°è¿”回 null
     */
    private String findMysqlDockerContainer(String dbPort) {
        try {
            Process ps = new ProcessBuilder("docker", "ps", "--format", "{{.ID}}|{{.Names}}|{{.Image}}")
                    .redirectErrorStream(true).start();
            String output = IoUtil.read(ps.getInputStream(), StandardCharsets.UTF_8);
            if (!ps.waitFor(10, TimeUnit.SECONDS) || ps.exitValue() != 0) {
                return null;
            }
            List<String[]> containers = new ArrayList<>();
            for (String line : output.split("\n")) {
                String[] parts = line.trim().split("\\|");
                if (parts.length == 3 && parts[2].startsWith("mysql")) {
                    containers.add(parts);
                }
            }
            if (containers.isEmpty()) {
                return null;
            }
            // ä¼˜å…ˆé€‰æ‹©ç«¯å£æ˜ å°„与数据源端口一致的容器
            for (String[] container : containers) {
                Process portProcess = new ProcessBuilder("docker", "port", container[0], "3306/tcp")
                        .redirectErrorStream(true).start();
                String portOutput = IoUtil.read(portProcess.getInputStream(), StandardCharsets.UTF_8);
                if (portProcess.waitFor(10, TimeUnit.SECONDS) && portProcess.exitValue() == 0
                        && portOutput.contains(dbPort)) {
                    return container[1];
                }
            }
            log.warn("未匹配到端口映射的 MySQL å®¹å™¨ï¼Œä½¿ç”¨ç¬¬ä¸€ä¸ª: {}", containers.get(0)[1]);
            return containers.get(0)[1];
        } catch (IOException e) {
            log.warn("检测 Docker çŽ¯å¢ƒå¤±è´¥: {}", e.getMessage());
            return null;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        }
    }
}
yudao-server/src/main/resources/application-dev.yaml
@@ -154,3 +154,10 @@
    compress: true # æ˜¯å¦åŽ‹ç¼©å›¾ç‰‡
    need-compress-size: 10MB # è§¦å‘压缩的文件大小阈值
    compress-quality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
  database-backup: # æ•°æ®åº“备份配置
    host: 127.0.0.1 # å¤‡ä»½ç›®æ ‡ MySQL ä¸»æœºï¼Œç•™ç©ºè‡ªåŠ¨ä»Žæ•°æ®æº JDBC URL è§£æž
    port: 3306 # å¤‡ä»½ç›®æ ‡ MySQL ç«¯å£ï¼Œç•™ç©ºè‡ªåŠ¨ä»Žæ•°æ®æº JDBC URL è§£æž
    database: ruoyi-vue-pro # å¤‡ä»½ç›®æ ‡æ•°æ®åº“,留空自动从数据源 JDBC URL è§£æž
    username: root # å¤‡ä»½è´¦å·ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºè´¦å·
    password: '123456' # å¤‡ä»½å¯†ç ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºå¯†ç 
    command: # mysqldump å‘½ä»¤ï¼Œç•™ç©ºè‡ªåŠ¨æ£€æµ‹éƒ¨ç½²çŽ¯å¢ƒï¼ˆæœ¬æœº mysqldump æˆ– Docker MySQL å®¹å™¨ï¼‰ï¼›å¯æ‰‹åŠ¨æŒ‡å®šï¼Œå¦‚ï¼šdocker exec mysql8 mysqldump
yudao-server/src/main/resources/application-jhhg.yaml
@@ -220,3 +220,10 @@
    compress: true # æ˜¯å¦åŽ‹ç¼©å›¾ç‰‡
    need-compress-size: 10MB # è§¦å‘压缩的文件大小阈值
    compress-quality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
  database-backup: # æ•°æ®åº“备份配置
    host: 172.17.0.1 # å¤‡ä»½ç›®æ ‡ MySQL ä¸»æœºï¼Œç•™ç©ºè‡ªåŠ¨ä»Žæ•°æ®æº JDBC URL è§£æž
    port: 9002 # å¤‡ä»½ç›®æ ‡ MySQL ç«¯å£ï¼Œç•™ç©ºè‡ªåŠ¨ä»Žæ•°æ®æº JDBC URL è§£æž
    database: ruoyi-vue-pro # å¤‡ä»½ç›®æ ‡æ•°æ®åº“,留空自动从数据源 JDBC URL è§£æž
    username: root # å¤‡ä»½è´¦å·ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºè´¦å·
    password: 'jhhg@123456..' # å¤‡ä»½å¯†ç ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºå¯†ç 
    command: # mysqldump å‘½ä»¤ï¼Œç•™ç©ºè‡ªåŠ¨æ£€æµ‹éƒ¨ç½²çŽ¯å¢ƒï¼ˆæœ¬æœº mysqldump æˆ– Docker MySQL å®¹å™¨ï¼‰ï¼›å¯æ‰‹åŠ¨æŒ‡å®šï¼Œå¦‚ï¼šdocker exec mysql8 mysqldump
yudao-server/src/main/resources/application-local.yaml
@@ -198,3 +198,10 @@
    compress: true # æ˜¯å¦åŽ‹ç¼©å›¾ç‰‡
    need-compress-size: 10MB # è§¦å‘压缩的文件大小阈值
    compress-quality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
  database-backup: # æ•°æ®åº“备份配置
    host: localhost # å¤‡ä»½ç›®æ ‡ MySQL ä¸»æœºï¼Œç•™ç©ºè‡ªåŠ¨ä»Žæ•°æ®æº JDBC URL è§£æž
    port: 3306 # å¤‡ä»½ç›®æ ‡ MySQL ç«¯å£ï¼Œç•™ç©ºè‡ªåŠ¨ä»Žæ•°æ®æº JDBC URL è§£æž
    database: ruoyi-vue-pro # å¤‡ä»½ç›®æ ‡æ•°æ®åº“,留空自动从数据源 JDBC URL è§£æž
    username: root # å¤‡ä»½è´¦å·ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºè´¦å·
    password: '123456' # å¤‡ä»½å¯†ç ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºå¯†ç 
    command: # mysqldump å‘½ä»¤ï¼Œç•™ç©ºè‡ªåŠ¨æ£€æµ‹éƒ¨ç½²çŽ¯å¢ƒï¼ˆæœ¬æœº mysqldump æˆ– Docker MySQL å®¹å™¨ï¼‰ï¼›å¯æ‰‹åŠ¨æŒ‡å®šï¼Œå¦‚ï¼šdocker exec mysql8 mysqldump
yudao-server/src/main/resources/application-test.yaml
@@ -220,3 +220,10 @@
    compress: true # æ˜¯å¦åŽ‹ç¼©å›¾ç‰‡
    need-compress-size: 10MB # è§¦å‘压缩的文件大小阈值
    compress-quality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
  database-backup: # æ•°æ®åº“备份配置
    host: 172.17.0.1 # å¤‡ä»½ç›®æ ‡ MySQL ä¸»æœºï¼Œç•™ç©ºè‡ªåŠ¨ä»Žæ•°æ®æº JDBC URL è§£æž
    port: 11101 # å¤‡ä»½ç›®æ ‡ MySQL ç«¯å£ï¼Œç•™ç©ºè‡ªåŠ¨ä»Žæ•°æ®æº JDBC URL è§£æž
    database: ruoyi-vue-pro # å¤‡ä»½ç›®æ ‡æ•°æ®åº“,留空自动从数据源 JDBC URL è§£æž
    username: root # å¤‡ä»½è´¦å·ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºè´¦å·
    password: 'xd@123456..' # å¤‡ä»½å¯†ç ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºå¯†ç 
    command: # mysqldump å‘½ä»¤ï¼Œç•™ç©ºè‡ªåŠ¨æ£€æµ‹éƒ¨ç½²çŽ¯å¢ƒï¼ˆæœ¬æœº mysqldump æˆ– Docker MySQL å®¹å™¨ï¼‰ï¼›å¯æ‰‹åŠ¨æŒ‡å®šï¼Œå¦‚ï¼šdocker exec mysql8 mysqldump
yudao-server/src/main/resources/application-wtxc.yaml
@@ -220,3 +220,10 @@
    compress: true # æ˜¯å¦åŽ‹ç¼©å›¾ç‰‡
    need-compress-size: 10MB # è§¦å‘压缩的文件大小阈值
    compress-quality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
  database-backup: # æ•°æ®åº“备份配置
    host: 172.17.0.1 # å¤‡ä»½ç›®æ ‡ MySQL ä¸»æœºï¼Œç•™ç©ºè‡ªåŠ¨ä»Žæ•°æ®æº JDBC URL è§£æž
    port: 9002 # å¤‡ä»½ç›®æ ‡ MySQL ç«¯å£ï¼Œç•™ç©ºè‡ªåŠ¨ä»Žæ•°æ®æº JDBC URL è§£æž
    database: mom-wtxc # å¤‡ä»½ç›®æ ‡æ•°æ®åº“,留空自动从数据源 JDBC URL è§£æž
    username: root # å¤‡ä»½è´¦å·ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºè´¦å·
    password: 'wtxc@123456..' # å¤‡ä»½å¯†ç ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºå¯†ç 
    command: # mysqldump å‘½ä»¤ï¼Œç•™ç©ºè‡ªåŠ¨æ£€æµ‹éƒ¨ç½²çŽ¯å¢ƒï¼ˆæœ¬æœº mysqldump æˆ– Docker MySQL å®¹å™¨ï¼‰ï¼›å¯æ‰‹åŠ¨æŒ‡å®šï¼Œå¦‚ï¼šdocker exec mysql8 mysqldump
yudao-server/src/main/resources/application-xjln.yaml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,198 @@
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:3306/mom-xjln?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
          username: root
          password: xd@123456..
  # 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 ï¼Œæˆ‘们手动创建表结构。
--- #################### æœåŠ¡ä¿éšœç›¸å…³é…ç½® ####################
# 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.system.dal.mysql: debug
    cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper: INFO # é…ç½® SmsChannelMapper çš„æ—¥å¿—级别为 info
    cn.iocoder.yudao.module.member.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.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_xjln
        host: 36.213.90.123
        port: 19530
--- #################### è¶…级管理员相关配置 ####################
# è¶…级管理员配置项,设置当前项目所有自定义的配置
yudao:
  captcha:
    enable: false # æœ¬åœ°çŽ¯å¢ƒï¼Œæš‚æ—¶å…³é—­å›¾ç‰‡éªŒè¯ç ï¼Œæ–¹ä¾¿ç™»å½•ç­‰æŽ¥å£çš„æµ‹è¯•ï¼›
  security:
    mock-enable: true
  access-log: # è®¿é—®æ—¥å¿—的配置项
    enable: false
  demo: false # å…³é—­æ¼”示模式
  storage: # æ–‡ä»¶ä¸Šä¼ é…ç½®
    path: /javaWork/mom/file # æ–‡ä»¶å­˜å‚¨æ ¹ç›®å½•
    url-prefix: /admin-api/system/storage-blob # URL路径前缀
    domain: http://114.132.189.42:9061 # åŸŸåå‰ç¼€ï¼ˆæŒ‰å®žé™…域名配置),前端配置好像不太对现在是直连后端,所有要后端端口
    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)
yudao-server/src/main/resources/application-yzfx.yaml
@@ -220,3 +220,10 @@
    compress: true # æ˜¯å¦åŽ‹ç¼©å›¾ç‰‡
    need-compress-size: 10MB # è§¦å‘压缩的文件大小阈值
    compress-quality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
  database-backup: # æ•°æ®åº“备份配置
    host: 172.17.0.1 # å¤‡ä»½ç›®æ ‡ MySQL ä¸»æœºï¼Œç•™ç©ºè‡ªåŠ¨ä»Žæ•°æ®æº JDBC URL è§£æž
    port: 9002 # å¤‡ä»½ç›®æ ‡ MySQL ç«¯å£ï¼Œç•™ç©ºè‡ªåŠ¨ä»Žæ•°æ®æº JDBC URL è§£æž
    database: ruoyi-vue-pro # å¤‡ä»½ç›®æ ‡æ•°æ®åº“,留空自动从数据源 JDBC URL è§£æž
    username: root # å¤‡ä»½è´¦å·ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºè´¦å·
    password: 'yzfx@123456..' # å¤‡ä»½å¯†ç ï¼Œç•™ç©ºä½¿ç”¨æ•°æ®æºå¯†ç 
    command: # mysqldump å‘½ä»¤ï¼Œç•™ç©ºè‡ªåŠ¨æ£€æµ‹éƒ¨ç½²çŽ¯å¢ƒï¼ˆæœ¬æœº mysqldump æˆ– Docker MySQL å®¹å™¨ï¼‰ï¼›å¯æ‰‹åŠ¨æŒ‡å®šï¼Œå¦‚ï¼šdocker exec mysql8 mysqldump