From 8f7c42bb0bced4697755299f67483be11da3e44d Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 30 七月 2026 17:25:21 +0800
Subject: [PATCH] feat(aftersales): 新增售后模块数据权限管理和维修退货功能
---
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java | 18
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java | 10
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnvendor/MesWmReturnVendorDO.java | 4
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/AfterSalePermissionServiceImpl.java | 64
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/permission/AfterSalePermissionMapper.java | 48
yudao-server/pom.xml | 7
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketItemDO.java | 78 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorRespVO.java | 3
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleRepairStatusEnum.java | 32
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketRespVO.java | 151 ++
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/returnobj/AfterSaleReturnDO.java | 112 +
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/dto/MesWmReturnSalesCreateReqDTO.java | 44
yudao-module-aftersales/pom.xml | 118 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApiImpl.java | 27
docs/sql/aftersales_init.sql | 170 ++
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApi.java | 20
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnPageReqVO.java | 33
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/AfterSaleReturnController.java | 95 +
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java | 4
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java | 3
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java | 221 ++
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/permission/AfterSalePermissionDO.java | 55
docs/aftersales_frontend_integration.md | 636 ++++++++
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketPageReqVO.java | 39
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/LogRecordConstants.java | 48
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/util/AfterSalePermissionUtils.java | 94 +
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketService.java | 113 +
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/returnobj/AfterSaleReturnMapper.java | 48
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/productsales/MesWmProductSalesLineMapper.java | 22
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java | 5
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnService.java | 92 +
yudao-module-aftersales-api/pom.xml | 33
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketItemMapper.java | 26
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleBizTypeEnum.java | 40
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/AfterSaleTicketController.java | 165 ++
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/permission/AfterSalePermissionLevelEnum.java | 58
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleReturnStatusEnum.java | 37
yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/AfterSaleTicketApi.java | 54
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleIssueTypeEnum.java | 32
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java | 4
yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/dto/AfterSaleTicketRespDTO.java | 31
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketMapper.java | 58
pom.xml | 3
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/repair/AfterSaleRepairDO.java | 78 +
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSaveReqVO.java | 89 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorSaveReqVO.java | 4
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/repair/AfterSaleRepairMapper.java | 34
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/productsales/MesWmProductSalesLineApi.java | 21
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestRespVO.java | 3
yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/dto/AfterSaleReturnRespDTO.java | 32
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketDO.java | 116 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesSaveReqVO.java | 7
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesRespVO.java | 6
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnRespVO.java | 139 +
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/returnobj/AfterSaleReturnItemDO.java | 78 +
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairServiceImpl.java | 110 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnsales/MesWmReturnSalesDO.java | 8
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/api/AfterSaleTicketApiImpl.java | 59
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/returnobj/AfterSaleReturnItemMapper.java | 26
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairService.java | 63
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/RedisKeyConstants.java | 16
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesPageReqVO.java | 11
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderPageReqVO.java | 9
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleTicketStatusEnum.java | 35
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnSaveReqVO.java | 88 +
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/bo/AfterSalePermissionCreateReqBO.java | 38
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/permission/AfterSaleSceneTypeEnum.java | 54
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java | 46
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java | 212 ++
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/ErrorCodeConstants.java | 38
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/AfterSalePermissionService.java | 61
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/productsales/MesWmProductSalesLineApiImpl.java | 28
72 files changed, 4,441 insertions(+), 23 deletions(-)
diff --git a/docs/aftersales_frontend_integration.md b/docs/aftersales_frontend_integration.md
new file mode 100644
index 0000000..7144ac3
--- /dev/null
+++ b/docs/aftersales_frontend_integration.md
@@ -0,0 +1,636 @@
+# 鍞悗宸ュ崟妯″潡 - 鍓嶇鑱旇皟鏂规
+
+## 娑夊強椤甸潰锛堝叏閮ㄦ柊寤猴級
+
+| 椤甸潰 | 璺敱寤鸿 | 璇存槑 |
+|------|---------|------|
+| 鍞悗宸ュ崟鍒楄〃 | `/aftersales/ticket` | 宸ュ崟鍒楄〃銆佹悳绱€�佺姸鎬佺瓫閫� |
+| 鍞悗宸ュ崟璇︽儏/缂栬緫 | `/aftersales/ticket/detail?id=:id` | 鏂板缓/缂栬緫宸ュ崟銆佹煡鐪嬭鎯� |
+| 閫�璐х敵璇峰垪琛� | `/aftersales/return` | 閫�璐х敵璇峰垪琛ㄣ�佹悳绱� |
+| 閫�璐х敵璇疯鎯� | `/aftersales/return/detail?id=:id` | 閫�璐ц鎯呫�佸鏍告搷浣� |
+
+## 涓氬姟娴佺▼涓庢暟鎹甫鍏�
+
+### 娴佺▼涓�锛氫竴鑸棶棰樺伐鍗�
+
+```
+鍒涘缓宸ュ崟 鈫� 鍒嗛厤澶勭悊浜� 鈫� 闂鍒ゅ畾锛堜竴鑸棶棰橈級鈫� 澶勭悊 鈫� 瀹岀粨 鈫� 鍏抽棴
+```
+
+### 娴佺▼浜岋細閫�璐ч棶棰樺伐鍗�
+
+```
+鍒涘缓宸ュ崟 鈫� 鍒嗛厤澶勭悊浜� 鈫� 闂鍒ゅ畾锛堥��璐ч棶棰橈級
+ 鈫� 鍒涘缓閫�璐х敵璇� 鈫� 閿�鍞鏍� 鈫� [MES閫�璐ф祦绋嬭嚜鍔ㄦ墽琛宂 鈫� 閫�娆�/鎹㈣揣瀹屾垚 鈫� 瀹岀粨 鈫� 鍏抽棴
+```
+
+### 娴佺▼涓夛細缁翠慨闂宸ュ崟
+
+```
+鍒涘缓宸ュ崟 鈫� 鍒嗛厤澶勭悊浜� 鈫� 闂鍒ゅ畾锛堢淮淇棶棰橈級
+ 鈫� 鍒涘缓缁翠慨璁板綍 鈫� 缁翠慨瀹屾垚 鈫� 瀹岀粨 鈫� 鍏抽棴
+```
+
+### 鏁版嵁甯﹀叆鍏崇郴
+
+| 姝ラ | 鏉ユ簮 | 甯﹀叆瀛楁 |
+|------|------|----------|
+| 鍒涘缓閫�璐х敵璇� | 宸ュ崟璇︽儏 | `ticketId`銆乣customerId`銆乣saleOrderId`銆佸伐鍗曟槑缁嗚锛坄ticketItemId`銆乣itemId`銆乣itemCode`銆乣itemName`锛� |
+| 閫�璐х敵璇峰鎵� | 閫�璐х敵璇� | `id`锛堥�氳繃 URL 鍙傛暟鎴栨寜閽紶鍏ワ級 |
+
+**閲嶇偣璇存槑锛�**
+- 鍒涘缓閫�璐х敵璇锋椂锛屼粠宸ュ崟璇︽儏椤电殑鏄庣粏琛屼腑绛涢�� `needReturn = true` 鐨勮浣滀负閫�璐ф槑缁�
+- 閫�璐ф槑缁嗕腑鐨� `ticketItemId` 蹇呴』瀵瑰簲宸ュ崟鏄庣粏琛岀殑 `id`
+- 鐗╂枡淇℃伅锛坄itemId`銆乣itemCode`銆乣itemName`锛変粠宸ュ崟鏄庣粏琛屽甫鍏ワ紝涓嶅厑璁镐慨鏀�
+- 閿�鍞崟浠凤紙`unitPrice`锛夐渶瑕佷粠閿�鍞鍗曡鏌ヨ鑾峰彇锛屽墠绔彲閫氳繃宸叉湁鐨� ERP 閿�鍞鍗曟帴鍙f煡璇�
+
+---
+
+## API 璇存槑
+
+### 涓�銆佸敭鍚庡伐鍗�
+
+| 鏂规硶 | 璺緞 | 鏉冮檺 | 璇存槑 |
+|------|------|------|------|
+| POST | `/aftersales/ticket/create` | `aftersales:ticket:create` | 鍒涘缓宸ュ崟 |
+| PUT | `/aftersales/ticket/update` | `aftersales:ticket:update` | 鏇存柊宸ュ崟锛堜粎鑽夌鐘舵�佸彲鏇存柊锛� |
+| DELETE | `/aftersales/ticket/delete` | `aftersales:ticket:delete` | 鍒犻櫎宸ュ崟锛堜粎鑽夌/宸插彇娑堢姸鎬佸彲鍒犻櫎锛� |
+| GET | `/aftersales/ticket/get` | `aftersales:ticket:query` | 鑾峰彇宸ュ崟璇︽儏 |
+| GET | `/aftersales/ticket/page` | `aftersales:ticket:query` | 鍒嗛〉鏌ヨ宸ュ崟 |
+| POST | `/aftersales/ticket/assign` | `aftersales:ticket:update` | 鍒嗛厤澶勭悊浜� |
+| POST | `/aftersales/ticket/classify` | `aftersales:ticket:update` | 闂鍒ゅ畾 |
+| PUT | `/aftersales/ticket/resolve` | `aftersales:ticket:update` | 瀹岀粨宸ュ崟 |
+| PUT | `/aftersales/ticket/close` | `aftersales:ticket:update` | 鍏抽棴宸ュ崟 |
+| PUT | `/aftersales/ticket/cancel` | `aftersales:ticket:update` | 鍙栨秷宸ュ崟 |
+
+#### 1.1 鍒涘缓宸ュ崟 `POST /aftersales/ticket/create`
+
+**璇锋眰鍙傛暟锛�**
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| name | String | 鏄� | 宸ュ崟涓婚 |
+| customerId | Long | 鏄� | 瀹㈡埛缂栧彿 |
+| contactId | Long | 鍚� | 鑱旂郴浜虹紪鍙� |
+| saleOrderId | Long | 鍚� | 閿�鍞鍗曠紪鍙� |
+| saleOrderNo | String | 鍚� | 閿�鍞鍗曞彿锛堝啑浣欙紝涓� saleOrderId 閰嶅浼犲叆锛� |
+| contractId | Long | 鍚� | 鍚堝悓缂栧彿 |
+| issueDescription | String | 鍚� | 闂鎻忚堪 |
+| remark | String | 鍚� | 澶囨敞 |
+| items | List\<Item\> | 鍚� | 鏄庣粏琛屽垪琛� |
+
+**Item 缁撴瀯锛�**
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| saleOrderItemId | Long | 鍚� | 閿�鍞鍗曡缂栧彿 |
+| itemId | Long | 鏄� | MDM 鐗╂枡缂栧彿 |
+| itemCode | String | 鍚� | 鐗╂枡缂栫爜 |
+| itemName | String | 鍚� | 鐗╂枡鍚嶇О |
+| batchId | Long | 鍚� | 鐢熶骇鎵规缂栧彿 |
+| batchCode | String | 鍚� | 鐢熶骇鎵规鍙� |
+| saleQuantity | BigDecimal | 鍚� | 閿�鍞暟閲� |
+| returnQuantity | BigDecimal | 鍚� | 閫�璐ф暟閲� |
+| needRepair | Boolean | 鍚� | 鏄惁闇�瑕佺淮淇� |
+| needReturn | Boolean | 鍚� | 鏄惁闇�瑕侀��璐� |
+| remark | String | 鍚� | 澶囨敞 |
+
+**璇锋眰绀轰緥锛�**
+```json
+{
+ "name": "浜у搧澶栬缂洪櫡鎶曡瘔",
+ "customerId": 1,
+ "contactId": 2,
+ "saleOrderId": 100,
+ "saleOrderNo": "SO202401010001",
+ "contractId": 50,
+ "issueDescription": "鏀跺埌璐у悗鍙戠幇浜у搧琛ㄩ潰鏈夋槑鏄惧垝鐥�",
+ "remark": "瀹㈡埛瑕佹眰灏藉揩澶勭悊",
+ "items": [
+ {
+ "saleOrderItemId": 10,
+ "itemId": 200,
+ "itemCode": "MAT-001",
+ "itemName": "鐢靛瓙鍏冨櫒浠�",
+ "batchId": 5,
+ "batchCode": "BATCH-2024-001",
+ "saleQuantity": 100.000,
+ "returnQuantity": 10.000,
+ "needRepair": false,
+ "needReturn": true,
+ "remark": "澶栬缂洪櫡"
+ }
+ ]
+}
+```
+
+**鍝嶅簲锛�** `{ "code": 0, "data": 1 }` 鈥� data 涓烘柊寤哄伐鍗� ID
+
+#### 1.2 鏇存柊宸ュ崟 `PUT /aftersales/ticket/update`
+
+璇锋眰鍙傛暟涓庡垱寤虹浉鍚岋紝棰濆闇�瑕� `id` 瀛楁銆備粎鑽夌鐘舵�佸彲鏇存柊銆�
+
+#### 1.3 鍒犻櫎宸ュ崟 `DELETE /aftersales/ticket/delete`
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏄� | 宸ュ崟缂栧彿锛圦uery 鍙傛暟锛� |
+
+**鍝嶅簲锛�** `{ "code": 0, "data": true }`
+
+#### 1.4 鑾峰彇宸ュ崟璇︽儏 `GET /aftersales/ticket/get`
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏄� | 宸ュ崟缂栧彿锛圦uery 鍙傛暟锛� |
+
+**鍝嶅簲瀛楁锛�**
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| id | Long | 宸ュ崟缂栧彿 |
+| no | String | 宸ュ崟鍗曞彿锛堟牸寮忥細AS + yyyyMMdd + 6浣嶅簭鍙凤級 |
+| name | String | 宸ュ崟涓婚 |
+| customerId | Long | 瀹㈡埛缂栧彿 |
+| customerName | String | 瀹㈡埛鍚嶇О |
+| contactId | Long | 鑱旂郴浜虹紪鍙� |
+| contactName | String | 鑱旂郴浜哄悕绉� |
+| saleOrderId | Long | 閿�鍞鍗曠紪鍙� |
+| saleOrderNo | String | 閿�鍞鍗曞彿 |
+| contractId | Long | 鍚堝悓缂栧彿 |
+| issueType | Integer | 闂绫诲瀷锛�1-涓�鑸棶棰� 2-缁翠慨闂 3-閫�璐ч棶棰� |
+| issueCategory | String | 闂鍒嗙被 |
+| issueDescription | String | 闂鎻忚堪 |
+| severityLevel | Integer | 涓ラ噸绋嬪害锛�0-杞诲井 1-涓�鑸� 2-涓ラ噸 3-鑷村懡 |
+| status | Integer | 鐘舵�侊紙瑙佺姸鎬佹灇涓撅級 |
+| ownerUserId | Long | 璐熻矗浜虹敤鎴风紪鍙� |
+| ownerUserName | String | 璐熻矗浜哄悕绉� |
+| handlerUserId | Long | 澶勭悊浜虹敤鎴风紪鍙� |
+| handlerUserName | String | 澶勭悊浜哄悕绉� |
+| processInstanceId | String | BPM 宸ヤ綔娴佸疄渚嬬紪鍙� |
+| returnId | Long | 鍏宠仈閫�璐х敵璇风紪鍙� |
+| repairId | Long | 鍏宠仈缁翠慨璁板綍缂栧彿 |
+| resolvedTime | DateTime | 瑙e喅鏃堕棿 |
+| closedTime | DateTime | 鍏抽棴鏃堕棿 |
+| remark | String | 澶囨敞 |
+| creator | String | 鍒涘缓鑰� |
+| createTime | DateTime | 鍒涘缓鏃堕棿 |
+| updater | String | 鏇存柊鑰� |
+| updateTime | DateTime | 鏇存柊鏃堕棿 |
+| items | List\<Item\> | 鏄庣粏琛屽垪琛� |
+
+**鍝嶅簲绀轰緥锛�**
+```json
+{
+ "code": 0,
+ "data": {
+ "id": 1,
+ "no": "AS20260730000001",
+ "name": "浜у搧澶栬缂洪櫡鎶曡瘔",
+ "customerId": 1,
+ "customerName": "XX绉戞妧鏈夐檺鍏徃",
+ "contactId": 2,
+ "contactName": "寮犱笁",
+ "saleOrderId": 100,
+ "saleOrderNo": "SO202401010001",
+ "contractId": 50,
+ "issueType": 3,
+ "issueCategory": "澶栬缂洪櫡",
+ "issueDescription": "鏀跺埌璐у悗鍙戠幇浜у搧琛ㄩ潰鏈夋槑鏄惧垝鐥�",
+ "severityLevel": 1,
+ "status": 30,
+ "ownerUserId": 1,
+ "ownerUserName": "绠$悊鍛�",
+ "handlerUserId": 2,
+ "handlerUserName": "鍞悗涓撳憳",
+ "returnId": 1,
+ "items": [
+ {
+ "id": 1,
+ "ticketId": 1,
+ "saleOrderItemId": 10,
+ "itemId": 200,
+ "itemCode": "MAT-001",
+ "itemName": "鐢靛瓙鍏冨櫒浠�",
+ "batchId": 5,
+ "batchCode": "BATCH-2024-001",
+ "saleQuantity": 100.000,
+ "returnQuantity": 10.000,
+ "needRepair": false,
+ "needReturn": true
+ }
+ ]
+ }
+}
+```
+
+#### 1.5 鍒嗛〉鏌ヨ宸ュ崟 `GET /aftersales/ticket/page`
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| pageNo | Integer | 鍚� | 椤电爜锛岄粯璁� 1 |
+| pageSize | Integer | 鍚� | 姣忛〉澶у皬锛岄粯璁� 10 |
+| no | String | 鍚� | 宸ュ崟鍗曞彿 |
+| name | String | 鍚� | 宸ュ崟涓婚 |
+| customerId | Long | 鍚� | 瀹㈡埛缂栧彿 |
+| saleOrderId | Long | 鍚� | 閿�鍞鍗曠紪鍙� |
+| status | Integer | 鍚� | 宸ュ崟鐘舵�� |
+| issueType | Integer | 鍚� | 闂绫诲瀷 |
+| sceneType | Integer | 鍚� | 鍦烘櫙绫诲瀷锛�1-鎴戣礋璐g殑 2-鎴戝弬涓庣殑 3-涓嬪睘璐熻矗鐨� 4-鍏ㄩ儴 |
+
+**鍝嶅簲锛�**
+```json
+{
+ "code": 0,
+ "data": {
+ "list": [ /* AfterSaleTicketRespVO 鏁扮粍 */ ],
+ "total": 100
+ }
+}
+```
+
+**娉ㄦ剰锛�** 鍒嗛〉鍒楄〃鍝嶅簲涓殑姣忔潯璁板綍缁撴瀯涓庤鎯呮帴鍙d竴鑷达紙鍚� `items` 鍜屽叧鑱斿悕绉板瓧娈碉級锛屼絾鍒楄〃鍦烘櫙涓� `items` 鍙负绌恒��
+
+#### 1.6 鍒嗛厤澶勭悊浜� `POST /aftersales/ticket/assign`
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏄� | 宸ュ崟缂栧彿锛圦uery 鍙傛暟锛� |
+| handlerUserId | Long | 鏄� | 澶勭悊浜虹敤鎴风紪鍙凤紙Query 鍙傛暟锛� |
+
+**涓氬姟瑙勫垯锛�** 浠呰崏绋裤�佸鐞嗕腑鐘舵�佸彲鍒嗛厤銆傚垎閰嶅悗鐘舵�佽嚜鍔ㄥ彉涓�"澶勭悊涓�"銆�
+
+#### 1.7 闂鍒ゅ畾 `POST /aftersales/ticket/classify`
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏄� | 宸ュ崟缂栧彿锛圦uery 鍙傛暟锛� |
+| issueType | Integer | 鏄� | 闂绫诲瀷锛�1-涓�鑸棶棰� 2-缁翠慨闂 3-閫�璐ч棶棰� |
+| issueCategory | String | 鍚� | 闂鍒嗙被锛堝"澶栬缂洪櫡"銆�"鍔熻兘鏁呴殰"绛夛級 |
+| severityLevel | Integer | 鍚� | 涓ラ噸绋嬪害锛�0-杞诲井 1-涓�鑸� 2-涓ラ噸 3-鑷村懡 |
+
+**涓氬姟瑙勫垯锛�** 浠呭鐞嗕腑鐘舵�佸彲鍒ゅ畾銆傚垽瀹氫负閫�璐ч棶棰樺悗锛屽簲寮曞鐢ㄦ埛鍒涘缓閫�璐х敵璇枫��
+
+#### 1.8 瀹岀粨宸ュ崟 `PUT /aftersales/ticket/resolve`
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏄� | 宸ュ崟缂栧彿锛圦uery 鍙傛暟锛� |
+
+**涓氬姟瑙勫垯锛�** 浠呭鐞嗕腑/寰呴��璐�/寰呴��娆剧姸鎬佸彲瀹岀粨銆�
+
+#### 1.9 鍏抽棴宸ュ崟 `PUT /aftersales/ticket/close`
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏄� | 宸ュ崟缂栧彿锛圦uery 鍙傛暟锛� |
+
+**涓氬姟瑙勫垯锛�** 浠呭凡瀹岀粨鐘舵�佸彲鍏抽棴銆�
+
+#### 1.10 鍙栨秷宸ュ崟 `PUT /aftersales/ticket/cancel`
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏄� | 宸ュ崟缂栧彿锛圦uery 鍙傛暟锛� |
+
+**涓氬姟瑙勫垯锛�** 浠呰崏绋裤�佸鐞嗕腑鐘舵�佸彲鍙栨秷銆�
+
+---
+
+### 浜屻�佸敭鍚庨��璐х敵璇�
+
+| 鏂规硶 | 璺緞 | 鏉冮檺 | 璇存槑 |
+|------|------|------|------|
+| POST | `/aftersales/return/create` | `aftersales:return:create` | 鍒涘缓閫�璐х敵璇� |
+| PUT | `/aftersales/return/approve` | `aftersales:return:approve` | 瀹℃牳閫氳繃锛堣Е鍙� MES 閫�璐у崟锛� |
+| GET | `/aftersales/return/get` | `aftersales:return:query` | 鑾峰彇閫�璐ц鎯� |
+| GET | `/aftersales/return/page` | `aftersales:return:query` | 鍒嗛〉鏌ヨ閫�璐х敵璇� |
+
+#### 2.1 鍒涘缓閫�璐х敵璇� `POST /aftersales/return/create`
+
+**璇锋眰鍙傛暟锛�**
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| ticketId | Long | 鏄� | 鍏宠仈宸ュ崟缂栧彿 |
+| customerId | Long | 鏄� | 瀹㈡埛缂栧彿锛堜粠宸ュ崟甯﹀叆锛� |
+| saleOrderId | Long | 鍚� | 鍏宠仈閿�鍞鍗曠紪鍙凤紙浠庡伐鍗曞甫鍏ワ級 |
+| returnType | Integer | 鏄� | 閫�璐х被鍨嬶細1-閫�娆鹃��璐� 2-鎹㈣揣 3-浠呴��娆� |
+| returnReason | String | 鏄� | 閫�璐у師鍥� |
+| defectCategory | String | 鍚� | 缂洪櫡鍒嗙被 |
+| remark | String | 鍚� | 澶囨敞 |
+| items | List\<Item\> | 鏄� | 閫�璐ф槑缁嗚鍒楄〃 |
+
+**Item 缁撴瀯锛�**
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| ticketItemId | Long | 鏄� | 宸ュ崟鏄庣粏琛岀紪鍙凤紙鍏宠仈 after_sale_ticket_item.id锛� |
+| itemId | Long | 鏄� | MDM 鐗╂枡缂栧彿锛堜粠宸ュ崟鏄庣粏琛屽甫鍏ワ級 |
+| itemCode | String | 鍚� | 鐗╂枡缂栫爜 |
+| itemName | String | 鍚� | 鐗╂枡鍚嶇О |
+| batchId | Long | 鍚� | 鎵规缂栧彿 |
+| batchCode | String | 鍚� | 鎵规鍙� |
+| returnQuantity | BigDecimal | 鏄� | 閫�璐ф暟閲� |
+| unitPrice | BigDecimal | 鏄� | 閿�鍞崟浠凤紙闇�浠庨攢鍞鍗曡鏌ヨ鑾峰彇锛� |
+| returnPrice | BigDecimal | 鍚� | 閫�璐ч噾棰濓紙= returnQuantity 脳 unitPrice锛� |
+| remark | String | 鍚� | 澶囨敞 |
+
+**璇锋眰绀轰緥锛�**
+```json
+{
+ "ticketId": 1,
+ "customerId": 1,
+ "saleOrderId": 100,
+ "returnType": 1,
+ "returnReason": "浜у搧澶栬缂洪櫡",
+ "defectCategory": "澶栬缂洪櫡",
+ "remark": "瀹㈡埛瑕佹眰閫�璐ч��娆�",
+ "items": [
+ {
+ "ticketItemId": 1,
+ "itemId": 200,
+ "itemCode": "MAT-001",
+ "itemName": "鐢靛瓙鍏冨櫒浠�",
+ "batchId": 5,
+ "batchCode": "BATCH-2024-001",
+ "returnQuantity": 10.000,
+ "unitPrice": 100.00,
+ "returnPrice": 1000.00
+ }
+ ]
+}
+```
+
+**鍝嶅簲锛�** `{ "code": 0, "data": 1 }` 鈥� data 涓烘柊寤洪��璐х敵璇� ID
+
+**涓氬姟瑙勫垯锛�**
+- 鍒涘缓閫�璐х敵璇峰悗锛屽叧鑱斿伐鍗曠姸鎬佽嚜鍔ㄥ彉涓�"寰呴��璐�(30)"
+- `returnPrice` 鍚庣浼氳嚜鍔ㄨ绠楋紙`returnQuantity 脳 unitPrice` 姹傚拰锛夛紝鍓嶇鍙笉浼�
+
+#### 2.2 瀹℃牳閫氳繃 `PUT /aftersales/return/approve`
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏄� | 閫�璐х敵璇风紪鍙凤紙Query 鍙傛暟锛� |
+
+**涓氬姟瑙勫垯锛�**
+- 浠呰崏绋跨姸鎬�(0)鍙鏍�
+- 瀹℃牳閫氳繃鍚庯紝鍚庣鑷姩璋冪敤 MES 鍒涘缓閿�鍞��璐у崟锛岀姸鎬佸彉涓�"瀹℃牳閫氳繃-宸插悓姝ES(20)"
+- 鍚庣画 MES 閫�璐ф祦绋嬶紙璐ㄦ鈫掑叆搴撯啋閫�娆撅級鐢卞悗绔洖璋冭嚜鍔ㄥ畬鎴愶紝鍓嶇鏃犻渶鎿嶄綔
+
+#### 2.3 鑾峰彇閫�璐ц鎯� `GET /aftersales/return/get`
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏄� | 閫�璐х敵璇风紪鍙凤紙Query 鍙傛暟锛� |
+
+**鍝嶅簲瀛楁锛堟柊澧�/閲嶈瀛楁锛夛細**
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| id | Long | 閫�璐х敵璇风紪鍙� |
+| no | String | 閫�璐у崟鍙凤紙鏍煎紡锛歊T + yyyyMMdd + 6浣嶅簭鍙凤級 |
+| ticketId | Long | 鍏宠仈宸ュ崟缂栧彿 |
+| customerId | Long | 瀹㈡埛缂栧彿 |
+| customerName | String | 瀹㈡埛鍚嶇О |
+| saleOrderId | Long | 閿�鍞鍗曠紪鍙� |
+| returnType | Integer | 閫�璐х被鍨� |
+| returnReason | String | 閫�璐у師鍥� |
+| defectCategory | String | 缂洪櫡鍒嗙被 |
+| totalReturnPrice | BigDecimal | 搴旈��鎬婚噾棰� |
+| actualRefundPrice | BigDecimal | 瀹為��閲戦 |
+| status | Integer | 鐘舵�侊紙瑙侀��璐х姸鎬佹灇涓撅級 |
+| auditUserId | Long | 瀹℃牳浜� |
+| auditTime | DateTime | 瀹℃牳鏃堕棿 |
+| mesReturnId | Long | MES閫�璐у崟缂栧彿 |
+| mesReturnCode | String | MES閫�璐у崟鍙� |
+| qualityResult | String | 璐ㄦ缁撹 |
+| qualityReportUrl | String | 璐ㄦ鎶ュ憡闄勪欢URL |
+| returnInboundTime | DateTime | 閫�璐у叆搴撳畬鎴愭椂闂� |
+| refundTime | DateTime | 閫�娆惧畬鎴愭椂闂� |
+| items | List\<Item\> | 閫�璐ф槑缁嗚 |
+
+#### 2.4 鍒嗛〉鏌ヨ閫�璐х敵璇� `GET /aftersales/return/page`
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| pageNo | Integer | 鍚� | 椤电爜 |
+| pageSize | Integer | 鍚� | 姣忛〉澶у皬 |
+| no | String | 鍚� | 閫�璐у崟鍙� |
+| ticketId | Long | 鍚� | 鍏宠仈宸ュ崟缂栧彿 |
+| customerId | Long | 鍚� | 瀹㈡埛缂栧彿 |
+| status | Integer | 鍚� | 閫�璐х姸鎬� |
+| sceneType | Integer | 鍚� | 鍦烘櫙绫诲瀷 |
+
+---
+
+## 鏋氫妇鍊奸�熸煡
+
+### 宸ュ崟鐘舵�� (AfterSaleTicketStatusEnum)
+
+| 鍊� | 鍚嶇О | 璇存槑 |
+|----|------|------|
+| 0 | 鑽夌 | 鍙紪杈戙�佸彲鍒犻櫎銆佸彲鍙栨秷 |
+| 10 | 澶勭悊涓� | 鍙垎閰嶃�佸彲鍒ゅ畾銆佸彲鍙栨秷 |
+| 30 | 寰呴��璐� | 閫�璐ф祦绋嬭繘琛屼腑 |
+| 40 | 寰呴��娆�/鎹㈣揣 | 绛夊緟閫�娆炬垨鎹㈣揣瀹屾垚 |
+| 50 | 宸插畬缁� | 鍙叧闂� |
+| 60 | 宸插叧闂� | 缁堟�� |
+| 99 | 宸插彇娑� | 缁堟�� |
+
+### 闂绫诲瀷 (AfterSaleIssueTypeEnum)
+
+| 鍊� | 鍚嶇О | 鍚庣画鎿嶄綔 |
+|----|------|----------|
+| 0 | 鏈垽瀹� | 鍒涘缓宸ュ崟鏃剁殑鍒濆鍊� |
+| 1 | 涓�鑸棶棰� | 鐩存帴澶勭悊鈫掑畬缁� |
+| 2 | 缁翠慨闂 | 鍒涘缓缁翠慨璁板綍鈫掔淮淇畬鎴愨啋瀹岀粨 |
+| 3 | 閫�璐ч棶棰� | 鍒涘缓閫�璐х敵璇封啋瀹℃牳鈫扢ES閫�璐р啋瀹岀粨 |
+
+### 閫�璐х姸鎬� (AfterSaleReturnStatusEnum)
+
+| 鍊� | 鍚嶇О | 璇存槑 |
+|----|------|------|
+| 0 | 鑽夌 | 鍙鏍� |
+| 10 | 閿�鍞鏍镐腑 | 鈥� |
+| 20 | 瀹℃牳閫氳繃-宸插悓姝ES | 绛夊緟MES澶勭悊 |
+| 30 | 妫�楠屼腑 | MES璐ㄦ杩涜涓� |
+| 40 | 妫�楠屽畬鎴� | 绛夊緟鍏ュ簱 |
+| 50 | 宸插叆搴� | 绛夊緟閫�娆� |
+| 60 | 閫�娆�/鎹㈣揣瀹屾垚 | 鈥� |
+| 70 | 宸插叧闂� | 缁堟�� |
+| 99 | 宸插彇娑� | 缁堟�� |
+
+### 閫�璐х被鍨�
+
+| 鍊� | 鍚嶇О |
+|----|------|
+| 1 | 閫�娆鹃��璐� |
+| 2 | 鎹㈣揣 |
+| 3 | 浠呴��娆� |
+
+### 涓ラ噸绋嬪害
+
+| 鍊� | 鍚嶇О | 寤鸿棰滆壊 |
+|----|------|----------|
+| 0 | 杞诲井 | 缁胯壊 |
+| 1 | 涓�鑸� | 钃濊壊 |
+| 2 | 涓ラ噸 | 姗欒壊 |
+| 3 | 鑷村懡 | 绾㈣壊 |
+
+### 鍦烘櫙绫诲瀷 (sceneType)
+
+| 鍊� | 鍚嶇О | 璇存槑 |
+|----|------|------|
+| 1 | 鎴戣礋璐g殑 | 褰撳墠鐢ㄦ埛涓鸿礋璐d汉 |
+| 2 | 鎴戝弬涓庣殑 | 褰撳墠鐢ㄦ埛涓哄鐞嗕汉鎴栬鎺堟潈 |
+| 3 | 涓嬪睘璐熻矗鐨� | 涓嬪睘涓鸿礋璐d汉 |
+| 4 | 鍏ㄩ儴 | 绠$悊鍛樻煡鐪嬫墍鏈� |
+
+---
+
+## 瀛楁灞曠ず瑙勫垯
+
+### 宸ュ崟鍒楄〃椤�
+
+| 瀛楁 | 灞曠ず浣嶇疆 | 璇存槑 |
+|------|----------|------|
+| no | 鍒楄〃 | 鍗曞彿锛屽彲鐐瑰嚮杩涘叆璇︽儏 |
+| name | 鍒楄〃 | 宸ュ崟涓婚 |
+| customerName | 鍒楄〃 | 瀹㈡埛鍚嶇О |
+| saleOrderNo | 鍒楄〃 | 閿�鍞鍗曞彿 |
+| issueType | 鍒楄〃 | 闂绫诲瀷锛岀敤鏍囩鍖哄垎棰滆壊 |
+| severityLevel | 鍒楄〃 | 涓ラ噸绋嬪害锛岀敤鏍囩鍖哄垎棰滆壊 |
+| status | 鍒楄〃 | 鐘舵�侊紝鐢ㄦ爣绛惧尯鍒嗛鑹� |
+| ownerUserName | 鍒楄〃 | 璐熻矗浜� |
+| handlerUserName | 鍒楄〃 | 澶勭悊浜� |
+| createTime | 鍒楄〃 | 鍒涘缓鏃堕棿 |
+| 鈥� | 鎼滅储鍖� | 鍗曞彿銆佷富棰樸�佸鎴枫�佽鍗曘�佺姸鎬併�侀棶棰樼被鍨� |
+| 鈥� | 鎼滅储鍖� | 鍦烘櫙绫诲瀷 Tab锛氭垜璐熻矗鐨�/鎴戝弬涓庣殑/涓嬪睘璐熻矗鐨�/鍏ㄩ儴 |
+
+### 宸ュ崟璇︽儏椤�
+
+| 瀛楁 | 灞曠ず鍖哄煙 | 璇存槑 |
+|------|----------|------|
+| no | 澶撮儴 | 鍗曞彿 |
+| name | 澶撮儴 | 宸ュ崟涓婚 |
+| status | 澶撮儴 | 鐘舵�佹爣绛� + 鐘舵�佹祦杞寜閽� |
+| customerId / customerName | 鍩烘湰淇℃伅 | 瀹㈡埛閫夋嫨鍣紙鍒涘缓鏃跺彲閫夛紝鍒涘缓鍚庡彧璇伙級 |
+| contactId / contactName | 鍩烘湰淇℃伅 | 鑱旂郴浜洪�夋嫨鍣� |
+| saleOrderId / saleOrderNo | 鍩烘湰淇℃伅 | 閿�鍞鍗曢�夋嫨鍣� |
+| contractId | 鍩烘湰淇℃伅 | 鍚堝悓閫夋嫨鍣� |
+| issueType | 鍩烘湰淇℃伅 | 闂绫诲瀷锛堝垽瀹氬悗鏄剧ず锛� |
+| issueCategory | 鍩烘湰淇℃伅 | 闂鍒嗙被锛堝垽瀹氬悗鏄剧ず锛� |
+| issueDescription | 鍩烘湰淇℃伅 | 闂鎻忚堪 |
+| severityLevel | 鍩烘湰淇℃伅 | 涓ラ噸绋嬪害锛堝垽瀹氬悗鏄剧ず锛� |
+| ownerUserId / ownerUserName | 鍩烘湰淇℃伅 | 璐熻矗浜猴紙鍒涘缓鏃惰嚜鍔ㄨ涓哄綋鍓嶇敤鎴凤級 |
+| handlerUserId / handlerUserName | 鍩烘湰淇℃伅 | 澶勭悊浜猴紙鍒嗛厤鍚庢樉绀猴級 |
+| returnId | 鍩烘湰淇℃伅 | 鍏宠仈閫�璐х敵璇凤紙鍙偣鍑昏烦杞級 |
+| repairId | 鍩烘湰淇℃伅 | 鍏宠仈缁翠慨璁板綍锛堝彲鐐瑰嚮璺宠浆锛� |
+| resolvedTime | 鍩烘湰淇℃伅 | 瑙e喅鏃堕棿锛堝畬缁撳悗鏄剧ず锛� |
+| closedTime | 鍩烘湰淇℃伅 | 鍏抽棴鏃堕棿锛堝叧闂悗鏄剧ず锛� |
+| remark | 鍩烘湰淇℃伅 | 澶囨敞 |
+| items | 鏄庣粏琛ㄦ牸 | 鐗╂枡銆佹壒娆°�侀攢鍞暟閲忋�侀��璐ф暟閲忋�佹槸鍚︾淮淇�佹槸鍚﹂��璐� |
+| createTime / creator | 搴曢儴淇℃伅 | 鍒涘缓淇℃伅 |
+| updateTime / updater | 搴曢儴淇℃伅 | 鏇存柊淇℃伅 |
+
+### 閫�璐х敵璇峰垪琛ㄩ〉
+
+| 瀛楁 | 灞曠ず浣嶇疆 | 璇存槑 |
+|------|----------|------|
+| no | 鍒楄〃 | 閫�璐у崟鍙� |
+| ticketId | 鍒楄〃 | 鍏宠仈宸ュ崟缂栧彿锛堝彲鐐瑰嚮璺宠浆锛� |
+| customerName | 鍒楄〃 | 瀹㈡埛鍚嶇О |
+| returnType | 鍒楄〃 | 閫�璐х被鍨嬫爣绛� |
+| totalReturnPrice | 鍒楄〃 | 搴旈��鎬婚噾棰� |
+| actualRefundPrice | 鍒楄〃 | 瀹為��閲戦 |
+| status | 鍒楄〃 | 鐘舵�佹爣绛� |
+| mesReturnCode | 鍒楄〃 | MES閫�璐у崟鍙� |
+| qualityResult | 鍒楄〃 | 璐ㄦ缁撹 |
+| createTime | 鍒楄〃 | 鍒涘缓鏃堕棿 |
+
+### 閫�璐х敵璇疯鎯呴〉
+
+| 瀛楁 | 灞曠ず鍖哄煙 | 璇存槑 |
+|------|----------|------|
+| no | 澶撮儴 | 閫�璐у崟鍙� |
+| status | 澶撮儴 | 鐘舵�佹爣绛� + 瀹℃牳鎸夐挳锛堣崏绋跨姸鎬佹樉绀猴級 |
+| ticketId | 鍩烘湰淇℃伅 | 鍏宠仈宸ュ崟锛堝彲鐐瑰嚮璺宠浆锛� |
+| customerName | 鍩烘湰淇℃伅 | 瀹㈡埛鍚嶇О |
+| saleOrderId | 鍩烘湰淇℃伅 | 閿�鍞鍗� |
+| returnType | 鍩烘湰淇℃伅 | 閫�璐х被鍨� |
+| returnReason | 鍩烘湰淇℃伅 | 閫�璐у師鍥� |
+| defectCategory | 鍩烘湰淇℃伅 | 缂洪櫡鍒嗙被 |
+| totalReturnPrice | 鍩烘湰淇℃伅 | 搴旈��鎬婚噾棰� |
+| actualRefundPrice | 鍩烘湰淇℃伅 | 瀹為��閲戦 |
+| mesReturnId / mesReturnCode | MES淇℃伅 | MES閫�璐у崟 |
+| qualityResult | MES淇℃伅 | 璐ㄦ缁撹 |
+| qualityReportUrl | MES淇℃伅 | 璐ㄦ鎶ュ憡閾炬帴 |
+| returnInboundTime | MES淇℃伅 | 鍏ュ簱鏃堕棿 |
+| refundTime | 閫�娆句俊鎭� | 閫�娆炬椂闂� |
+| items | 鏄庣粏琛ㄦ牸 | 鐗╂枡銆佹壒娆°�侀��璐ф暟閲忋�佸崟浠枫�侀��璐ч噾棰� |
+| createTime / creator | 搴曢儴淇℃伅 | 鍒涘缓淇℃伅 |
+
+---
+
+## 涓氬姟瑙勫垯璇存槑
+
+### 宸ュ崟鎿嶄綔鎸夐挳鏄鹃殣瑙勫垯
+
+| 鎿嶄綔 | 鑽夌(0) | 澶勭悊涓�(10) | 寰呴��璐�(30) | 寰呴��娆�(40) | 宸插畬缁�(50) | 宸插叧闂�(60) | 宸插彇娑�(99) |
+|------|---------|------------|------------|------------|------------|------------|------------|
+| 缂栬緫 | 鉁� | 鉂� | 鉂� | 鉂� | 鉂� | 鉂� | 鉂� |
+| 鍒犻櫎 | 鉁� | 鉂� | 鉂� | 鉂� | 鉂� | 鉂� | 鉁� |
+| 鍒嗛厤澶勭悊浜� | 鉁� | 鉁� | 鉂� | 鉂� | 鉂� | 鉂� | 鉂� |
+| 闂鍒ゅ畾 | 鉂� | 鉁� | 鉂� | 鉂� | 鉂� | 鉂� | 鉂� |
+| 鍒涘缓閫�璐х敵璇� | 鉂� | 鉁�(鍒ゅ畾涓洪��璐у悗) | 鉂� | 鉂� | 鉂� | 鉂� | 鉂� |
+| 瀹岀粨 | 鉂� | 鉁� | 鉁� | 鉁� | 鉂� | 鉂� | 鉂� |
+| 鍏抽棴 | 鉂� | 鉂� | 鉂� | 鉂� | 鉁� | 鉂� | 鉂� |
+| 鍙栨秷 | 鉁� | 鉁� | 鉂� | 鉂� | 鉂� | 鉂� | 鉂� |
+
+### 閫�璐х敵璇锋搷浣滄寜閽樉闅愯鍒�
+
+| 鎿嶄綔 | 鑽夌(0) | 瀹℃牳涓�(10) | 宸插悓姝ES(20) | 妫�楠屼腑(30) | 妫�楠屽畬鎴�(40) | 宸插叆搴�(50) | 閫�娆惧畬鎴�(60) |
+|------|---------|------------|---------------|------------|--------------|------------|--------------|
+| 瀹℃牳閫氳繃 | 鉁� | 鉂� | 鉂� | 鉂� | 鉂� | 鉂� | 鉂� |
+| 鏌ョ湅璇︽儏 | 鉁� | 鉁� | 鉁� | 鉁� | 鉁� | 鉁� | 鉁� |
+
+### 鍒嗛〉鏌ヨ鏁版嵁鏉冮檺璇存槑
+
+- **sceneType=1锛堟垜璐熻矗鐨勶級**锛氭樉绀哄綋鍓嶇敤鎴蜂负 `ownerUserId` 鐨勫伐鍗� + 鏁版嵁浜ゆ帴鐨勫伐鍗�
+- **sceneType=2锛堟垜鍙備笌鐨勶級**锛氭樉绀哄綋鍓嶇敤鎴锋湁璇�/鍐欐潈闄愮殑宸ュ崟
+- **sceneType=3锛堜笅灞炶礋璐g殑锛�**锛氭樉绀哄綋鍓嶇敤鎴风殑涓嬪睘璐熻矗鐨勫伐鍗�
+- **sceneType=4锛堝叏閮級**锛氶渶瑕佺鐞嗗憳鏉冮檺锛坄aftersales:admin`锛�
+
+---
+
+## 鏂囦欢涓婁紶
+
+鍞悗妯″潡浣跨敤缁熶竴鐨勬枃浠朵笂浼犳満鍒讹紝涓嶈嚜琛屽疄鐜颁笂浼犳帴鍙c��
+
+**涓婁紶娴佺▼锛�**
+1. 璋冪敤 `POST /system/storage-blob/upload` 涓婁紶鏂囦欢 鈫� 鑾峰緱 `blobId`
+2. 璋冪敤 `POST /system/storage-attachment/bind` 缁戝畾鍒颁笟鍔¤褰�
+
+**recordType 鍙栧�硷細**
+
+| 涓氬姟瀵硅薄 | recordType |
+|----------|------------|
+| 鍞悗宸ュ崟 | `aftersales_ticket` |
+| 鍞悗閫�璐х敵璇� | `aftersales_return` |
+
+**鏌ヨ闄勪欢锛�** `GET /system/storage-attachment/list?recordType=aftersales_ticket&recordId=1`
+
+---
+
+## 娉ㄦ剰浜嬮」
+
+1. **鏄庣粏琛屾洿鏂扮瓥鐣�**锛氭洿鏂板伐鍗曟椂锛屾槑缁嗚閲囩敤"鍏堝垹鍚庡"绛栫暐锛屽墠绔繀椤讳紶鍏ュ畬鏁寸殑鏄庣粏琛屽垪琛紙鍖呮嫭鏈慨鏀圭殑琛岋級锛屽惁鍒欎細瀵艰嚧鏄庣粏琛屼涪澶便��
+2. **鍗曞彿鐢熸垚**锛氬伐鍗曞崟鍙凤紙AS鍓嶇紑锛夊拰閫�璐у崟鍙凤紙RT鍓嶇紑锛夌敱鍚庣鑷姩鐢熸垚锛屽墠绔笉闇�瑕佷紶鍏ャ��
+3. **鐘舵�佹祦杞�**锛氭墍鏈夌姸鎬佸彉鏇存搷浣滐紙鍒嗛厤銆佸垽瀹氥�佸畬缁撱�佸叧闂�佸彇娑堛�佸鏍革級鍚庣閮戒細鏍¢獙褰撳墠鐘舵�佹槸鍚﹀厑璁革紝鍓嶇搴旀牴鎹寜閽樉闅愯鍒欐帶鍒舵搷浣滃叆鍙c��
+4. **閫�璐у鏍稿悗鑷姩鍚屾 MES**锛氬鏍搁�氳繃閫�璐х敵璇峰悗锛屽悗绔嚜鍔ㄨ皟鐢� MES 鍒涘缓閿�鍞��璐у崟锛屽悗缁� MES 娴佺▼锛堣川妫�鈫掑叆搴撯啋閫�娆撅級鐢卞洖璋冭嚜鍔ㄦ帹杩涚姸鎬侊紝鍓嶇鍙渶杞鍒锋柊鏌ョ湅鏈�鏂扮姸鎬併��
+5. **鏁版嵁甯﹀叆**锛氬垱寤洪��璐х敵璇锋椂锛屽繀椤讳粠宸ュ崟璇︽儏椤佃烦杞苟甯﹀叆 `ticketId`銆乣customerId`銆乣saleOrderId` 浠ュ強宸ュ崟鏄庣粏琛屼腑鏍囪涓�"闇�瑕侀��璐�"鐨勮銆�
+6. **鏉冮檺鎺у埗**锛氭墍鏈夋帴鍙i兘鏈� `@PreAuthorize` 鏉冮檺鏍¢獙锛屽墠绔彍鍗曞拰鎸夐挳闇�瑕佸搴旈厤缃潈闄愭爣璇嗐��
+7. **璺敱鏉冮檺**锛氶渶鍦� `src/router/routes/modules/` 涓嬫柊寤� `aftersales.ts` 璺敱妯″潡鏂囦欢锛屽苟鍦ㄨ彍鍗曚腑娉ㄥ唽"鍞悗鏈嶅姟"涓�绾ц彍鍗曘��
+8. **API 妯″潡**锛氶渶鍦� `src/api/` 涓嬫柊寤� `aftersales/` 鐩綍锛屽垱寤� `ticket.ts` 鍜� `return.ts` 涓や釜 API 鏂囦欢銆�
diff --git a/docs/sql/aftersales_init.sql b/docs/sql/aftersales_init.sql
new file mode 100644
index 0000000..536020c
--- /dev/null
+++ b/docs/sql/aftersales_init.sql
@@ -0,0 +1,170 @@
+-- ============================================================
+-- 鍞悗宸ュ崟妯″潡 DDL
+-- ============================================================
+
+-- 1. 鍞悗宸ュ崟涓昏〃
+DROP TABLE IF EXISTS `after_sale_ticket`;
+CREATE TABLE `after_sale_ticket` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '宸ュ崟缂栧彿',
+ `no` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '宸ュ崟鍗曞彿',
+ `name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '宸ュ崟涓婚/鏍囬',
+ `customer_id` bigint NOT NULL COMMENT '瀹㈡埛缂栧彿锛屽叧鑱� crm_customer.id',
+ `contact_id` bigint DEFAULT NULL COMMENT '鑱旂郴浜虹紪鍙凤紝鍏宠仈 crm_contact.id',
+ `sale_order_id` bigint DEFAULT NULL COMMENT '閿�鍞鍗曠紪鍙凤紝鍏宠仈 erp_sale_order.id',
+ `sale_order_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '閿�鍞鍗曞彿(鍐椾綑)',
+ `contract_id` bigint DEFAULT NULL COMMENT '鍚堝悓缂栧彿锛屽叧鑱� crm_contract.id',
+ `issue_type` tinyint NOT NULL DEFAULT 0 COMMENT '闂绫诲瀷: 0-鏈垽瀹� 1-涓�鑸棶棰� 2-缁翠慨闂 3-閫�璐ч棶棰�',
+ `issue_category` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '闂鍒嗙被',
+ `issue_description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '闂鎻忚堪',
+ `severity_level` tinyint DEFAULT 0 COMMENT '涓ラ噸绋嬪害: 0-杞诲井 1-涓�鑸� 2-涓ラ噸 3-鑷村懡',
+ `status` tinyint NOT NULL DEFAULT 0 COMMENT '鐘舵��: 0-鑽夌 10-澶勭悊涓� 30-寰呴��璐� 40-寰呴��娆�/鎹㈣揣 50-宸插畬缁� 60-宸插叧闂� 99-宸插彇娑�',
+ `owner_user_id` bigint NOT NULL COMMENT '璐熻矗浜虹敤鎴风紪鍙�',
+ `handler_user_id` bigint DEFAULT NULL COMMENT '澶勭悊浜虹敤鎴风紪鍙�',
+ `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'BPM宸ヤ綔娴佸疄渚嬬紪鍙�',
+ `return_id` bigint DEFAULT NULL COMMENT '鍏宠仈閫�璐х敵璇风紪鍙�',
+ `repair_id` bigint DEFAULT NULL COMMENT '鍏宠仈缁翠慨璁板綍缂栧彿',
+ `resolved_time` datetime DEFAULT NULL COMMENT '瑙e喅鏃堕棿',
+ `closed_time` datetime DEFAULT NULL COMMENT '鍏抽棴鏃堕棿',
+ `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '澶囨敞',
+ `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '鍒涘缓鑰�',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '鏇存柊鑰�',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍒犻櫎',
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE KEY `uk_no` (`no`),
+ KEY `idx_customer_id` (`customer_id`),
+ KEY `idx_sale_order_id` (`sale_order_id`),
+ KEY `idx_owner_user_id` (`owner_user_id`),
+ KEY `idx_status` (`status`),
+ KEY `idx_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='鍞悗宸ュ崟涓昏〃';
+
+-- 2. 鍞悗宸ュ崟鏄庣粏琛岃〃
+DROP TABLE IF EXISTS `after_sale_ticket_item`;
+CREATE TABLE `after_sale_ticket_item` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '缂栧彿',
+ `ticket_id` bigint NOT NULL COMMENT '宸ュ崟缂栧彿锛屽叧鑱� after_sale_ticket.id',
+ `sale_order_item_id` bigint DEFAULT NULL COMMENT '閿�鍞鍗曡缂栧彿',
+ `item_id` bigint NOT NULL COMMENT 'MDM鐗╂枡缂栧彿',
+ `item_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '鐗╂枡缂栫爜(鍐椾綑)',
+ `item_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '鐗╂枡鍚嶇О(鍐椾綑)',
+ `batch_id` bigint DEFAULT NULL COMMENT '鐢熶骇鎵规缂栧彿',
+ `batch_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '鐢熶骇鎵规鍙�',
+ `sale_quantity` decimal(10,3) NOT NULL DEFAULT 0.000 COMMENT '閿�鍞暟閲�',
+ `return_quantity` decimal(10,3) DEFAULT 0.000 COMMENT '閫�璐ф暟閲�',
+ `need_repair` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁闇�瑕佺淮淇�',
+ `need_return` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁闇�瑕侀��璐�',
+ `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '澶囨敞',
+ `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '鍒涘缓鑰�',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '鏇存柊鑰�',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍒犻櫎',
+ PRIMARY KEY (`id`) USING BTREE,
+ KEY `idx_ticket_id` (`ticket_id`),
+ KEY `idx_item_id` (`item_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='鍞悗宸ュ崟鏄庣粏琛岃〃';
+
+-- 3. 閫�璐х敵璇疯〃
+DROP TABLE IF EXISTS `after_sale_return`;
+CREATE TABLE `after_sale_return` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '閫�璐х敵璇风紪鍙�',
+ `no` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '閫�璐у崟鍙�',
+ `ticket_id` bigint NOT NULL COMMENT '鍏宠仈宸ュ崟缂栧彿锛屽叧鑱� after_sale_ticket.id',
+ `customer_id` bigint NOT NULL COMMENT '瀹㈡埛缂栧彿',
+ `sale_order_id` bigint DEFAULT NULL COMMENT '鍏宠仈閿�鍞鍗曠紪鍙�',
+ `return_type` tinyint NOT NULL COMMENT '閫�璐х被鍨�: 1-閫�娆鹃��璐� 2-鎹㈣揣 3-浠呴��娆�',
+ `return_reason` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '閫�璐у師鍥�',
+ `defect_category` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '缂洪櫡鍒嗙被',
+ `total_return_price` decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '搴旈��鎬婚噾棰�',
+ `actual_refund_price` decimal(12,2) DEFAULT 0.00 COMMENT '瀹為��閲戦',
+ `status` tinyint NOT NULL DEFAULT 0 COMMENT '鐘舵��: 0-鑽夌 10-閿�鍞鏍镐腑 20-瀹℃牳閫氳繃宸插悓姝ES 30-妫�楠屼腑 40-妫�楠屽畬鎴� 50-宸插叆搴� 60-閫�娆�/鎹㈣揣瀹屾垚 70-宸插叧闂� 99-宸插彇娑�',
+ `audit_user_id` bigint DEFAULT NULL COMMENT '瀹℃牳浜�',
+ `audit_time` datetime DEFAULT NULL COMMENT '瀹℃牳鏃堕棿',
+ `mes_return_id` bigint DEFAULT NULL COMMENT 'MES閫�璐у崟缂栧彿锛屽叧鑱� mes_wm_return_sales.id',
+ `mes_return_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'MES閫�璐у崟鍙�(鍐椾綑)',
+ `quality_result` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '璐ㄦ缁撹: 鍚堟牸/鐗归噰/閫�璐�/鎶ュ簾',
+ `quality_report_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '璐ㄦ鎶ュ憡闄勪欢URL',
+ `return_inbound_time` datetime DEFAULT NULL COMMENT '閫�璐у叆搴撳畬鎴愭椂闂�',
+ `refund_time` datetime DEFAULT NULL COMMENT '閫�娆�/鎹㈣揣瀹屾垚鏃堕棿',
+ `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '澶囨敞',
+ `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '鍒涘缓鑰�',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '鏇存柊鑰�',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍒犻櫎',
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE KEY `uk_no` (`no`),
+ KEY `idx_ticket_id` (`ticket_id`),
+ KEY `idx_mes_return_id` (`mes_return_id`),
+ KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='鍞悗閫�璐х敵璇疯〃';
+
+-- 4. 閫�璐ф槑缁嗚琛�
+DROP TABLE IF EXISTS `after_sale_return_item`;
+CREATE TABLE `after_sale_return_item` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '缂栧彿',
+ `return_id` bigint NOT NULL COMMENT '閫�璐х敵璇风紪鍙凤紝鍏宠仈 after_sale_return.id',
+ `ticket_item_id` bigint NOT NULL COMMENT '宸ュ崟鏄庣粏琛岀紪鍙凤紝鍏宠仈 after_sale_ticket_item.id',
+ `item_id` bigint NOT NULL COMMENT 'MDM鐗╂枡缂栧彿',
+ `item_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '鐗╂枡缂栫爜(鍐椾綑)',
+ `item_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '鐗╂枡鍚嶇О(鍐椾綑)',
+ `batch_id` bigint DEFAULT NULL COMMENT '鎵规缂栧彿',
+ `batch_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '鎵规鍙�',
+ `return_quantity` decimal(10,3) NOT NULL COMMENT '閫�璐ф暟閲�',
+ `unit_price` decimal(12,2) NOT NULL COMMENT '閿�鍞崟浠�',
+ `return_price` decimal(12,2) NOT NULL COMMENT '閫�璐ч噾棰�',
+ `mes_return_line_id` bigint DEFAULT NULL COMMENT 'MES閫�璐у崟琛岀紪鍙�',
+ `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '澶囨敞',
+ `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '鍒涘缓鑰�',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '鏇存柊鑰�',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍒犻櫎',
+ PRIMARY KEY (`id`) USING BTREE,
+ KEY `idx_return_id` (`return_id`),
+ KEY `idx_ticket_item_id` (`ticket_item_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='鍞悗閫�璐ф槑缁嗚琛�';
+
+-- 5. 缁翠慨璁板綍琛�
+DROP TABLE IF EXISTS `after_sale_repair`;
+CREATE TABLE `after_sale_repair` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '缁翠慨璁板綍缂栧彿',
+ `ticket_id` bigint NOT NULL COMMENT '鍏宠仈宸ュ崟缂栧彿锛屽叧鑱� after_sale_ticket.id',
+ `repair_type` tinyint NOT NULL COMMENT '缁翠慨绫诲瀷: 1-鐜板満缁翠慨 2-杩斿巶缁翠慨 3-杩滅▼鎸囧',
+ `fault_description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '鏁呴殰鎻忚堪',
+ `repair_action` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '缁翠慨鎺柦',
+ `repair_result` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '缁翠慨缁撴灉: 宸蹭慨澶�/鏃犳硶淇/閮ㄥ垎淇',
+ `repair_cost` decimal(10,2) DEFAULT 0.00 COMMENT '缁翠慨璐圭敤',
+ `repair_user_id` bigint DEFAULT NULL COMMENT '缁翠慨浜�',
+ `status` tinyint NOT NULL DEFAULT 0 COMMENT '鐘舵��: 0-寰呭鐞� 10-缁翠慨涓� 20-宸插畬鎴� 30-宸插叧闂�',
+ `start_time` datetime DEFAULT NULL COMMENT '寮�濮嬬淮淇椂闂�',
+ `finish_time` datetime DEFAULT NULL COMMENT '瀹屾垚缁翠慨鏃堕棿',
+ `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '澶囨敞',
+ `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '鍒涘缓鑰�',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '鏇存柊鑰�',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍒犻櫎',
+ PRIMARY KEY (`id`) USING BTREE,
+ KEY `idx_ticket_id` (`ticket_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='鍞悗缁翠慨璁板綍琛�';
+
+-- 6. 鍞悗鏁版嵁鏉冮檺琛�
+DROP TABLE IF EXISTS `after_sale_permission`;
+CREATE TABLE `after_sale_permission` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '缂栧彿',
+ `biz_type` tinyint NOT NULL COMMENT '鏁版嵁绫诲瀷: 1-鍞悗宸ュ崟 2-閫�璐х敵璇� 3-缁翠慨璁板綍',
+ `biz_id` bigint NOT NULL COMMENT '鏁版嵁缂栧彿',
+ `user_id` bigint NOT NULL COMMENT '鐢ㄦ埛缂栧彿',
+ `level` tinyint NOT NULL COMMENT '鏉冮檺绾у埆: 1-璐熻矗浜� 2-鍙 3-璇诲啓',
+ `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '鍒涘缓鑰�',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '鏇存柊鑰�',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍒犻櫎',
+ PRIMARY KEY (`id`) USING BTREE,
+ KEY `idx_biz` (`biz_type`, `biz_id`),
+ KEY `idx_user` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='鍞悗鏁版嵁鏉冮檺琛�';
diff --git a/pom.xml b/pom.xml
index 930f493..72800d2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,6 +38,9 @@
<!-- SRM 渚涘簲鍟嗗叧绯荤鐞嗘ā鍧� -->
<module>yudao-module-srm-api</module>
<module>yudao-module-srm</module>
+ <!-- 鍞悗宸ュ崟妯″潡 -->
+ <module>yudao-module-aftersales-api</module>
+ <module>yudao-module-aftersales</module>
<!-- 璇峰弬鑰� https://doc.iocoder.cn/ai/build/ 鏂囨。锛屽畬鎴� AI 妯″潡鐨勫惎鍔紒锛侊紒 -->
<!-- <module>yudao-module-ai</module>-->
</modules>
diff --git a/yudao-module-aftersales-api/pom.xml b/yudao-module-aftersales-api/pom.xml
new file mode 100644
index 0000000..d1a3ca3
--- /dev/null
+++ b/yudao-module-aftersales-api/pom.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>yudao-module-aftersales-api</artifactId>
+ <packaging>jar</packaging>
+
+ <name>${project.artifactId}</name>
+ <description>
+ 鍞悗宸ュ崟妯″潡鐨� API 鎺ュ彛瀹氫箟锛屼緵鍏朵粬妯″潡寮曠敤
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-common</artifactId>
+ </dependency>
+
+ <!-- Swagger 娉ㄨВ -->
+ <dependency>
+ <groupId>org.springdoc</groupId>
+ <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/AfterSaleTicketApi.java b/yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/AfterSaleTicketApi.java
new file mode 100644
index 0000000..f3567d7
--- /dev/null
+++ b/yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/AfterSaleTicketApi.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.aftersales.api;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.aftersales.api.dto.AfterSaleReturnRespDTO;
+import cn.iocoder.yudao.module.aftersales.api.dto.AfterSaleTicketRespDTO;
+
+import java.math.BigDecimal;
+
+/**
+ * 鍞悗宸ュ崟 API 鎺ュ彛锛堜緵 MES/ERP 璺ㄦā鍧楄皟鐢級
+ */
+public interface AfterSaleTicketApi {
+
+ String PREFIX = "/rpc-api/aftersales/ticket";
+
+ /**
+ * MES 閫�璐ц川妫�瀹屾垚鍚庡洖璋�
+ *
+ * @param returnId 閫�璐х敵璇风紪鍙�
+ * @param qualityResult 璐ㄦ缁撹
+ * @param reportUrl 璐ㄦ鎶ュ憡 URL
+ */
+ CommonResult<Boolean> onReturnQualityFinished(Long returnId, String qualityResult, String reportUrl);
+
+ /**
+ * MES 閫�璐у叆搴撳畬鎴愬悗鍥炶皟
+ *
+ * @param returnId 閫�璐х敵璇风紪鍙�
+ */
+ CommonResult<Boolean> onReturnInboundFinished(Long returnId);
+
+ /**
+ * ERP 閫�娆惧畬鎴愬悗鍥炶皟
+ *
+ * @param returnId 閫�璐х敵璇风紪鍙�
+ * @param actualRefundPrice 瀹為��閲戦
+ */
+ CommonResult<Boolean> onRefundCompleted(Long returnId, BigDecimal actualRefundPrice);
+
+ /**
+ * 鏌ヨ鍞悗宸ュ崟锛堜緵 MES/ERP 鏌ヨ鍏宠仈淇℃伅锛�
+ *
+ * @param id 宸ュ崟缂栧彿
+ */
+ CommonResult<AfterSaleTicketRespDTO> getTicket(Long id);
+
+ /**
+ * 鏌ヨ閫�璐х敵璇凤紙渚� MES/ERP 鏌ヨ鍏宠仈淇℃伅锛�
+ *
+ * @param id 閫�璐х敵璇风紪鍙�
+ */
+ CommonResult<AfterSaleReturnRespDTO> getReturn(Long id);
+
+}
diff --git a/yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/dto/AfterSaleReturnRespDTO.java b/yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/dto/AfterSaleReturnRespDTO.java
new file mode 100644
index 0000000..7afc84f
--- /dev/null
+++ b/yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/dto/AfterSaleReturnRespDTO.java
@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.aftersales.api.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 鍞悗閫�璐х敵璇峰搷搴� DTO锛堜緵 RPC 璋冪敤浣跨敤锛�
+ */
+@Data
+public class AfterSaleReturnRespDTO {
+
+ private Long id;
+ private String no;
+ private Long ticketId;
+ private Long customerId;
+ private Long saleOrderId;
+ private Integer returnType;
+ private String returnReason;
+ private BigDecimal totalReturnPrice;
+ private BigDecimal actualRefundPrice;
+ private Integer status;
+ private Long mesReturnId;
+ private String mesReturnCode;
+ private String qualityResult;
+ private String qualityReportUrl;
+ private LocalDateTime returnInboundTime;
+ private LocalDateTime refundTime;
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/dto/AfterSaleTicketRespDTO.java b/yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/dto/AfterSaleTicketRespDTO.java
new file mode 100644
index 0000000..971339c
--- /dev/null
+++ b/yudao-module-aftersales-api/src/main/java/cn/iocoder/yudao/module/aftersales/api/dto/AfterSaleTicketRespDTO.java
@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.aftersales.api.dto;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 鍞悗宸ュ崟鍝嶅簲 DTO锛堜緵 RPC 璋冪敤浣跨敤锛�
+ */
+@Data
+public class AfterSaleTicketRespDTO {
+
+ private Long id;
+ private String no;
+ private String name;
+ private Long customerId;
+ private Long saleOrderId;
+ private String saleOrderNo;
+ private Integer issueType;
+ private String issueCategory;
+ private String issueDescription;
+ private Integer severityLevel;
+ private Integer status;
+ private Long ownerUserId;
+ private Long handlerUserId;
+ private Long returnId;
+ private Long repairId;
+ private LocalDateTime resolvedTime;
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-aftersales/pom.xml b/yudao-module-aftersales/pom.xml
new file mode 100644
index 0000000..ab92efd
--- /dev/null
+++ b/yudao-module-aftersales/pom.xml
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>yudao-module-aftersales</artifactId>
+ <packaging>jar</packaging>
+
+ <name>${project.artifactId}</name>
+ <description>
+ aftersales 鍖呬笅锛屽敭鍚庢湇鍔$鐞嗭紙After-sales Service Management锛夈��
+ 渚嬪璇达細鍞悗宸ュ崟銆侀��璐х敵璇枫�佺淮淇褰曠瓑绛�
+ </description>
+
+ <dependencies>
+ <!-- 鍞悗 API 妯″潡 -->
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-module-aftersales-api</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- CRM API 妯″潡 -->
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-module-crm-api</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-module-system</artifactId>
+ <version>${revision}</version>
+ </dependency>
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-module-infra</artifactId>
+ <version>${revision}</version>
+ </dependency>
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-module-bpm</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- MDM 涓绘暟鎹鐞� API -->
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-module-mdm-api</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- ERP 閿�鍞鍗� API -->
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-module-erp-api</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- MES API -->
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-module-mes-api</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- HRM API锛堟暟鎹氦鎺ワ級 -->
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-module-hrm-api</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
+ <!-- 涓氬姟缁勪欢 -->
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
+ </dependency>
+
+ <!-- Web 鐩稿叧 -->
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-spring-boot-starter-web</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-spring-boot-starter-security</artifactId>
+ </dependency>
+
+ <!-- DB 鐩稿叧 -->
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
+ </dependency>
+
+ <!-- 宸ュ叿绫荤浉鍏� -->
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-spring-boot-starter-excel</artifactId>
+ </dependency>
+
+ <!-- Test 娴嬭瘯鐩稿叧 -->
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-spring-boot-starter-test</artifactId>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/api/AfterSaleTicketApiImpl.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/api/AfterSaleTicketApiImpl.java
new file mode 100644
index 0000000..d636f28
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/api/AfterSaleTicketApiImpl.java
@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.module.aftersales.api;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.aftersales.api.dto.AfterSaleReturnRespDTO;
+import cn.iocoder.yudao.module.aftersales.api.dto.AfterSaleTicketRespDTO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnDO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
+import cn.iocoder.yudao.module.aftersales.service.returnobj.AfterSaleReturnService;
+import cn.iocoder.yudao.module.aftersales.service.ticket.AfterSaleTicketService;
+import jakarta.annotation.Resource;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.math.BigDecimal;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+/**
+ * 鍞悗宸ュ崟 API 瀹炵幇绫伙紙渚� MES/ERP 璺ㄦā鍧楄皟鐢級
+ */
+@RestController
+public class AfterSaleTicketApiImpl implements AfterSaleTicketApi {
+
+ @Resource
+ private AfterSaleTicketService ticketService;
+ @Resource
+ private AfterSaleReturnService returnService;
+
+ @Override
+ public CommonResult<Boolean> onReturnQualityFinished(Long returnId, String qualityResult, String reportUrl) {
+ returnService.onQualityFinished(returnId, qualityResult, reportUrl);
+ return success(true);
+ }
+
+ @Override
+ public CommonResult<Boolean> onReturnInboundFinished(Long returnId) {
+ returnService.onInboundFinished(returnId);
+ return success(true);
+ }
+
+ @Override
+ public CommonResult<Boolean> onRefundCompleted(Long returnId, BigDecimal actualRefundPrice) {
+ returnService.onRefundCompleted(returnId, actualRefundPrice);
+ return success(true);
+ }
+
+ @Override
+ public CommonResult<AfterSaleTicketRespDTO> getTicket(Long id) {
+ AfterSaleTicketDO ticket = ticketService.getTicket(id);
+ return success(BeanUtils.toBean(ticket, AfterSaleTicketRespDTO.class));
+ }
+
+ @Override
+ public CommonResult<AfterSaleReturnRespDTO> getReturn(Long id) {
+ AfterSaleReturnDO returnDO = returnService.getReturn(id);
+ return success(BeanUtils.toBean(returnDO, AfterSaleReturnRespDTO.class));
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/AfterSaleReturnController.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/AfterSaleReturnController.java
new file mode 100644
index 0000000..4f593cf
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/AfterSaleReturnController.java
@@ -0,0 +1,95 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.returnobj;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo.AfterSaleReturnPageReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo.AfterSaleReturnRespVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo.AfterSaleReturnSaveReqVO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnDO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnItemDO;
+import cn.iocoder.yudao.module.aftersales.service.returnobj.AfterSaleReturnService;
+import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
+import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
+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.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "绠$悊鍚庡彴 - 鍞悗閫�璐х敵璇�")
+@RestController
+@RequestMapping("/aftersales/return")
+@Validated
+public class AfterSaleReturnController {
+
+ @Resource
+ private AfterSaleReturnService returnService;
+
+ @Resource
+ private CrmCustomerApi customerApi;
+
+ @PostMapping("/create")
+ @Operation(summary = "鍒涘缓閫�璐х敵璇�")
+ @PreAuthorize("@ss.hasPermission('aftersales:return:create')")
+ public CommonResult<Long> createReturn(@Valid @RequestBody AfterSaleReturnSaveReqVO createReqVO) {
+ return success(returnService.createReturn(createReqVO, getLoginUserId()));
+ }
+
+ @PutMapping("/approve")
+ @Operation(summary = "瀹℃牳閫氳繃閫�璐х敵璇�")
+ @PreAuthorize("@ss.hasPermission('aftersales:return:approve')")
+ public CommonResult<Boolean> approveReturn(@RequestParam("id") Long id) {
+ returnService.approveReturn(id);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "鑾峰緱閫�璐х敵璇�")
+ @Parameter(name = "id", description = "閫�璐х敵璇风紪鍙�", required = true, example = "1")
+ @PreAuthorize("@ss.hasPermission('aftersales:return:query')")
+ public CommonResult<AfterSaleReturnRespVO> getReturn(@RequestParam("id") Long id) {
+ AfterSaleReturnDO returnDO = returnService.getReturn(id);
+ List<AfterSaleReturnItemDO> items = returnService.getReturnItemListByReturnId(id);
+ List<AfterSaleReturnRespVO> enrichedList = buildReturnDetailList(List.of(returnDO));
+ AfterSaleReturnRespVO respVO = enrichedList.get(0);
+ respVO.setItems(BeanUtils.toBean(items, AfterSaleReturnRespVO.Item.class));
+ return success(respVO);
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "鑾峰緱閫�璐х敵璇峰垎椤�")
+ @PreAuthorize("@ss.hasPermission('aftersales:return:query')")
+ public CommonResult<PageResult<AfterSaleReturnRespVO>> getReturnPage(@Valid AfterSaleReturnPageReqVO pageReqVO) {
+ PageResult<AfterSaleReturnDO> pageResult = returnService.getReturnPage(pageReqVO, getLoginUserId());
+ List<AfterSaleReturnRespVO> list = buildReturnDetailList(pageResult.getList());
+ return success(new PageResult<>(list, pageResult.getTotal()));
+ }
+
+ // ========== 鍐呴儴鏂规硶锛氭壒閲忔嫾瑁呭叧鑱斿悕绉� ==========
+
+ private List<AfterSaleReturnRespVO> buildReturnDetailList(List<AfterSaleReturnDO> returnList) {
+ if (CollUtil.isEmpty(returnList)) {
+ return Collections.emptyList();
+ }
+ // 鎵归噺鍔犺浇瀹㈡埛鍚嶇О
+ Set<Long> customerIds = convertSet(returnList, AfterSaleReturnDO::getCustomerId);
+ Map<Long, CrmCustomerRespDTO> customerMap = customerApi.getCustomerMap(customerIds);
+ // 鎷艰鍚嶇О瀛楁
+ return BeanUtils.toBean(returnList, AfterSaleReturnRespVO.class, vo -> {
+ CrmCustomerRespDTO customer = customerMap.get(vo.getCustomerId());
+ if (customer != null) vo.setCustomerName(customer.getName());
+ });
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnPageReqVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnPageReqVO.java
new file mode 100644
index 0000000..32340a0
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnPageReqVO.java
@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+/**
+ * 鍞悗閫�璐х敵璇峰垎椤垫煡璇� VO
+ */
+@Schema(description = "鍞悗閫�璐х敵璇� - 鍒嗛〉鏌ヨ Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class AfterSaleReturnPageReqVO extends PageParam {
+
+ @Schema(description = "閫�璐у崟鍙�", example = "RT20240101000001")
+ private String no;
+
+ @Schema(description = "鍏宠仈宸ュ崟缂栧彿", example = "1")
+ private Long ticketId;
+
+ @Schema(description = "瀹㈡埛缂栧彿", example = "1")
+ private Long customerId;
+
+ @Schema(description = "閫�璐х姸鎬�", example = "10")
+ private Integer status;
+
+ @Schema(description = "鍦烘櫙绫诲瀷", example = "1")
+ private Integer sceneType;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnRespVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnRespVO.java
new file mode 100644
index 0000000..007ef50
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnRespVO.java
@@ -0,0 +1,139 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 鍞悗閫�璐х敵璇峰搷搴� VO
+ */
+@Schema(description = "鍞悗閫�璐х敵璇� - 鍝嶅簲 VO")
+@Data
+public class AfterSaleReturnRespVO {
+
+ @Schema(description = "閫�璐х敵璇风紪鍙�", example = "1")
+ private Long id;
+
+ @Schema(description = "閫�璐у崟鍙�", example = "RT20240101000001")
+ private String no;
+
+ @Schema(description = "鍏宠仈宸ュ崟缂栧彿", example = "1")
+ private Long ticketId;
+
+ @Schema(description = "瀹㈡埛缂栧彿", example = "1")
+ private Long customerId;
+
+ @Schema(description = "瀹㈡埛鍚嶇О")
+ private String customerName;
+
+ @Schema(description = "鍏宠仈閿�鍞鍗曠紪鍙�", example = "1")
+ private Long saleOrderId;
+
+ @Schema(description = "閫�璐х被鍨�: 1-閫�娆鹃��璐� 2-鎹㈣揣 3-浠呴��娆�", example = "1")
+ private Integer returnType;
+
+ @Schema(description = "閫�璐у師鍥�", example = "浜у搧澶栬缂洪櫡")
+ private String returnReason;
+
+ @Schema(description = "缂洪櫡鍒嗙被", example = "澶栬缂洪櫡")
+ private String defectCategory;
+
+ @Schema(description = "搴旈��鎬婚噾棰�", example = "1000.00")
+ private BigDecimal totalReturnPrice;
+
+ @Schema(description = "瀹為��閲戦", example = "950.00")
+ private BigDecimal actualRefundPrice;
+
+ @Schema(description = "鐘舵��", example = "10")
+ private Integer status;
+
+ @Schema(description = "瀹℃牳浜�", example = "1")
+ private Long auditUserId;
+
+ @Schema(description = "瀹℃牳鏃堕棿")
+ private LocalDateTime auditTime;
+
+ @Schema(description = "MES閫�璐у崟缂栧彿", example = "1")
+ private Long mesReturnId;
+
+ @Schema(description = "MES閫�璐у崟鍙�", example = "MES-RT-001")
+ private String mesReturnCode;
+
+ @Schema(description = "璐ㄦ缁撹", example = "鍚堟牸")
+ private String qualityResult;
+
+ @Schema(description = "璐ㄦ鎶ュ憡闄勪欢URL")
+ private String qualityReportUrl;
+
+ @Schema(description = "閫�璐у叆搴撳畬鎴愭椂闂�")
+ private LocalDateTime returnInboundTime;
+
+ @Schema(description = "閫�娆�/鎹㈣揣瀹屾垚鏃堕棿")
+ private LocalDateTime refundTime;
+
+ @Schema(description = "澶囨敞")
+ private String remark;
+
+ @Schema(description = "鍒涘缓鑰�")
+ private String creator;
+
+ @Schema(description = "鍒涘缓鏃堕棿")
+ private LocalDateTime createTime;
+
+ @Schema(description = "鏇存柊鑰�")
+ private String updater;
+
+ @Schema(description = "鏇存柊鏃堕棿")
+ private LocalDateTime updateTime;
+
+ @Schema(description = "閫�璐ф槑缁嗚鍒楄〃")
+ private List<Item> items;
+
+ @Schema(description = "閫�璐ф槑缁嗚")
+ @Data
+ public static class Item {
+
+ @Schema(description = "缂栧彿", example = "1")
+ private Long id;
+
+ @Schema(description = "閫�璐х敵璇风紪鍙�", example = "1")
+ private Long returnId;
+
+ @Schema(description = "宸ュ崟鏄庣粏琛岀紪鍙�", example = "1")
+ private Long ticketItemId;
+
+ @Schema(description = "MDM鐗╂枡缂栧彿", example = "1")
+ private Long itemId;
+
+ @Schema(description = "鐗╂枡缂栫爜", example = "MAT-001")
+ private String itemCode;
+
+ @Schema(description = "鐗╂枡鍚嶇О", example = "鐢靛瓙鍏冨櫒浠�")
+ private String itemName;
+
+ @Schema(description = "鎵规缂栧彿", example = "1")
+ private Long batchId;
+
+ @Schema(description = "鎵规鍙�", example = "BATCH-2024-001")
+ private String batchCode;
+
+ @Schema(description = "閫�璐ф暟閲�", example = "10.000")
+ private BigDecimal returnQuantity;
+
+ @Schema(description = "閿�鍞崟浠�", example = "100.00")
+ private BigDecimal unitPrice;
+
+ @Schema(description = "閫�璐ч噾棰�", example = "1000.00")
+ private BigDecimal returnPrice;
+
+ @Schema(description = "MES閫�璐у崟琛岀紪鍙�", example = "1")
+ private Long mesReturnLineId;
+
+ @Schema(description = "澶囨敞")
+ private String remark;
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnSaveReqVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnSaveReqVO.java
new file mode 100644
index 0000000..6ff52ca
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/returnobj/vo/AfterSaleReturnSaveReqVO.java
@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 鍞悗閫�璐х敵璇蜂繚瀛� VO
+ */
+@Schema(description = "鍞悗閫�璐х敵璇� - 淇濆瓨 Request VO")
+@Data
+public class AfterSaleReturnSaveReqVO {
+
+ @Schema(description = "閫�璐х敵璇风紪鍙�", example = "1")
+ private Long id;
+
+ @Schema(description = "鍏宠仈宸ュ崟缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "鍏宠仈宸ュ崟涓嶈兘涓虹┖")
+ private Long ticketId;
+
+ @Schema(description = "瀹㈡埛缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "瀹㈡埛涓嶈兘涓虹┖")
+ private Long customerId;
+
+ @Schema(description = "鍏宠仈閿�鍞鍗曠紪鍙�", example = "1")
+ private Long saleOrderId;
+
+ @Schema(description = "閫�璐х被鍨�: 1-閫�娆鹃��璐� 2-鎹㈣揣 3-浠呴��娆�", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "閫�璐х被鍨嬩笉鑳戒负绌�")
+ private Integer returnType;
+
+ @Schema(description = "閫�璐у師鍥�", requiredMode = Schema.RequiredMode.REQUIRED, example = "浜у搧澶栬缂洪櫡")
+ @NotEmpty(message = "閫�璐у師鍥犱笉鑳戒负绌�")
+ private String returnReason;
+
+ @Schema(description = "缂洪櫡鍒嗙被", example = "澶栬缂洪櫡")
+ private String defectCategory;
+
+ @Schema(description = "澶囨敞")
+ private String remark;
+
+ @Schema(description = "閫�璐ф槑缁嗚鍒楄〃")
+ private List<Item> items;
+
+ @Schema(description = "閫�璐ф槑缁嗚")
+ @Data
+ public static class Item {
+
+ @Schema(description = "宸ュ崟鏄庣粏琛岀紪鍙�", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotNull(message = "宸ュ崟鏄庣粏琛屼笉鑳戒负绌�")
+ private Long ticketItemId;
+
+ @Schema(description = "MDM鐗╂枡缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotNull(message = "鐗╂枡涓嶈兘涓虹┖")
+ private Long itemId;
+
+ @Schema(description = "鐗╂枡缂栫爜", example = "MAT-001")
+ private String itemCode;
+
+ @Schema(description = "鐗╂枡鍚嶇О", example = "鐢靛瓙鍏冨櫒浠�")
+ private String itemName;
+
+ @Schema(description = "鎵规缂栧彿", example = "1")
+ private Long batchId;
+
+ @Schema(description = "鎵规鍙�", example = "BATCH-2024-001")
+ private String batchCode;
+
+ @Schema(description = "閫�璐ф暟閲�", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.000")
+ @NotNull(message = "閫�璐ф暟閲忎笉鑳戒负绌�")
+ private BigDecimal returnQuantity;
+
+ @Schema(description = "閿�鍞崟浠�", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+ @NotNull(message = "閿�鍞崟浠蜂笉鑳戒负绌�")
+ private BigDecimal unitPrice;
+
+ @Schema(description = "閫�璐ч噾棰�", example = "1000.00")
+ private BigDecimal returnPrice;
+
+ @Schema(description = "澶囨敞")
+ private String remark;
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/AfterSaleTicketController.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/AfterSaleTicketController.java
new file mode 100644
index 0000000..3acf172
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/AfterSaleTicketController.java
@@ -0,0 +1,165 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.ticket;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+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.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketPageReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketRespVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketSaveReqVO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketItemDO;
+import cn.iocoder.yudao.module.aftersales.service.ticket.AfterSaleTicketService;
+import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
+import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
+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.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "绠$悊鍚庡彴 - 鍞悗宸ュ崟")
+@RestController
+@RequestMapping("/aftersales/ticket")
+@Validated
+public class AfterSaleTicketController {
+
+ @Resource
+ private AfterSaleTicketService ticketService;
+
+ @Resource
+ private CrmCustomerApi customerApi;
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @PostMapping("/create")
+ @Operation(summary = "鍒涘缓鍞悗宸ュ崟")
+ @PreAuthorize("@ss.hasPermission('aftersales:ticket:create')")
+ public CommonResult<Long> createTicket(@Valid @RequestBody AfterSaleTicketSaveReqVO createReqVO) {
+ return success(ticketService.createTicket(createReqVO, getLoginUserId()));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "鏇存柊鍞悗宸ュ崟")
+ @PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
+ public CommonResult<Boolean> updateTicket(@Valid @RequestBody AfterSaleTicketSaveReqVO updateReqVO) {
+ ticketService.updateTicket(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "鍒犻櫎鍞悗宸ュ崟")
+ @Parameter(name = "id", description = "宸ュ崟缂栧彿", required = true)
+ @PreAuthorize("@ss.hasPermission('aftersales:ticket:delete')")
+ public CommonResult<Boolean> deleteTicket(@RequestParam("id") Long id) {
+ ticketService.deleteTicket(id);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "鑾峰緱鍞悗宸ュ崟")
+ @Parameter(name = "id", description = "宸ュ崟缂栧彿", required = true, example = "1")
+ @PreAuthorize("@ss.hasPermission('aftersales:ticket:query')")
+ public CommonResult<AfterSaleTicketRespVO> getTicket(@RequestParam("id") Long id) {
+ AfterSaleTicketDO ticket = ticketService.getTicket(id);
+ List<AfterSaleTicketItemDO> items = ticketService.getTicketItemListByTicketId(id);
+ // 鎵归噺鍔犺浇鍏宠仈鍚嶇О
+ List<AfterSaleTicketRespVO> enrichedList = buildTicketDetailList(List.of(ticket));
+ AfterSaleTicketRespVO respVO = enrichedList.get(0);
+ respVO.setItems(BeanUtils.toBean(items, AfterSaleTicketRespVO.Item.class));
+ return success(respVO);
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "鑾峰緱鍞悗宸ュ崟鍒嗛〉")
+ @PreAuthorize("@ss.hasPermission('aftersales:ticket:query')")
+ public CommonResult<PageResult<AfterSaleTicketRespVO>> getTicketPage(@Valid AfterSaleTicketPageReqVO pageReqVO) {
+ PageResult<AfterSaleTicketDO> pageResult = ticketService.getTicketPage(pageReqVO, getLoginUserId());
+ List<AfterSaleTicketRespVO> list = buildTicketDetailList(pageResult.getList());
+ return success(new PageResult<>(list, pageResult.getTotal()));
+ }
+
+ @PostMapping("/assign")
+ @Operation(summary = "鍒嗛厤澶勭悊浜�")
+ @PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
+ public CommonResult<Boolean> assignTicket(@RequestParam("id") Long id,
+ @RequestParam("handlerUserId") Long handlerUserId) {
+ ticketService.assignTicket(id, handlerUserId);
+ return success(true);
+ }
+
+ @PostMapping("/classify")
+ @Operation(summary = "闂鍒ゅ畾")
+ @PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
+ public CommonResult<Boolean> classifyTicket(@RequestParam("id") Long id,
+ @RequestParam("issueType") Integer issueType,
+ @RequestParam(value = "issueCategory", required = false) String issueCategory,
+ @RequestParam(value = "severityLevel", required = false) Integer severityLevel) {
+ ticketService.classifyTicket(id, issueType, issueCategory, severityLevel);
+ return success(true);
+ }
+
+ @PutMapping("/resolve")
+ @Operation(summary = "瀹岀粨宸ュ崟")
+ @PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
+ public CommonResult<Boolean> resolveTicket(@RequestParam("id") Long id) {
+ ticketService.resolveTicket(id);
+ return success(true);
+ }
+
+ @PutMapping("/close")
+ @Operation(summary = "鍏抽棴宸ュ崟")
+ @PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
+ public CommonResult<Boolean> closeTicket(@RequestParam("id") Long id) {
+ ticketService.closeTicket(id);
+ return success(true);
+ }
+
+ @PutMapping("/cancel")
+ @Operation(summary = "鍙栨秷宸ュ崟")
+ @PreAuthorize("@ss.hasPermission('aftersales:ticket:update')")
+ public CommonResult<Boolean> cancelTicket(@RequestParam("id") Long id) {
+ ticketService.cancelTicket(id);
+ return success(true);
+ }
+
+ // ========== 鍐呴儴鏂规硶锛氭壒閲忔嫾瑁呭叧鑱斿悕绉� ==========
+
+ private List<AfterSaleTicketRespVO> buildTicketDetailList(List<AfterSaleTicketDO> ticketList) {
+ if (CollUtil.isEmpty(ticketList)) {
+ return Collections.emptyList();
+ }
+ // 1. 鎵归噺鍔犺浇瀹㈡埛鍚嶇О
+ Set<Long> customerIds = convertSet(ticketList, AfterSaleTicketDO::getCustomerId);
+ Map<Long, CrmCustomerRespDTO> customerMap = customerApi.getCustomerMap(customerIds);
+ // 2. 鎵归噺鍔犺浇鐢ㄦ埛鍚嶇О锛堣礋璐d汉 + 澶勭悊浜猴級
+ Set<Long> userIds = new HashSet<>();
+ ticketList.forEach(t -> {
+ if (t.getOwnerUserId() != null) userIds.add(t.getOwnerUserId());
+ if (t.getHandlerUserId() != null) userIds.add(t.getHandlerUserId());
+ });
+ Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
+ // 3. 鎷艰鍚嶇О瀛楁
+ return BeanUtils.toBean(ticketList, AfterSaleTicketRespVO.class, vo -> {
+ CrmCustomerRespDTO customer = customerMap.get(vo.getCustomerId());
+ if (customer != null) vo.setCustomerName(customer.getName());
+ AdminUserRespDTO ownerUser = userMap.get(vo.getOwnerUserId());
+ if (ownerUser != null) vo.setOwnerUserName(ownerUser.getNickname());
+ AdminUserRespDTO handlerUser = userMap.get(vo.getHandlerUserId());
+ if (handlerUser != null) vo.setHandlerUserName(handlerUser.getNickname());
+ });
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketPageReqVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketPageReqVO.java
new file mode 100644
index 0000000..eef8955
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketPageReqVO.java
@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+/**
+ * 鍞悗宸ュ崟鍒嗛〉鏌ヨ VO
+ */
+@Schema(description = "鍞悗宸ュ崟 - 鍒嗛〉鏌ヨ Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class AfterSaleTicketPageReqVO extends PageParam {
+
+ @Schema(description = "宸ュ崟鍗曞彿", example = "AS20240101000001")
+ private String no;
+
+ @Schema(description = "宸ュ崟涓婚", example = "浜у搧澶栬缂洪櫡")
+ private String name;
+
+ @Schema(description = "瀹㈡埛缂栧彿", example = "1")
+ private Long customerId;
+
+ @Schema(description = "閿�鍞鍗曠紪鍙�", example = "1")
+ private Long saleOrderId;
+
+ @Schema(description = "宸ュ崟鐘舵��", example = "10")
+ private Integer status;
+
+ @Schema(description = "闂绫诲瀷", example = "1")
+ private Integer issueType;
+
+ @Schema(description = "鍦烘櫙绫诲瀷", example = "1")
+ private Integer sceneType;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketRespVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketRespVO.java
new file mode 100644
index 0000000..9b25b76
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketRespVO.java
@@ -0,0 +1,151 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 鍞悗宸ュ崟鍝嶅簲 VO
+ */
+@Schema(description = "鍞悗宸ュ崟 - 鍝嶅簲 VO")
+@Data
+public class AfterSaleTicketRespVO {
+
+ @Schema(description = "宸ュ崟缂栧彿", example = "1")
+ private Long id;
+
+ @Schema(description = "宸ュ崟鍗曞彿", example = "AS20240101000001")
+ private String no;
+
+ @Schema(description = "宸ュ崟涓婚", example = "浜у搧澶栬缂洪櫡")
+ private String name;
+
+ @Schema(description = "瀹㈡埛缂栧彿", example = "1")
+ private Long customerId;
+
+ @Schema(description = "瀹㈡埛鍚嶇О")
+ private String customerName;
+
+ @Schema(description = "鑱旂郴浜虹紪鍙�", example = "1")
+ private Long contactId;
+
+ @Schema(description = "鑱旂郴浜哄悕绉�")
+ private String contactName;
+
+ @Schema(description = "閿�鍞鍗曠紪鍙�", example = "1")
+ private Long saleOrderId;
+
+ @Schema(description = "閿�鍞鍗曞彿", example = "SO202401010001")
+ private String saleOrderNo;
+
+ @Schema(description = "鍚堝悓缂栧彿", example = "1")
+ private Long contractId;
+
+ @Schema(description = "闂绫诲瀷: 1-涓�鑸棶棰� 2-缁翠慨闂 3-閫�璐ч棶棰�", example = "1")
+ private Integer issueType;
+
+ @Schema(description = "闂鍒嗙被", example = "澶栬缂洪櫡")
+ private String issueCategory;
+
+ @Schema(description = "闂鎻忚堪", example = "鏀跺埌璐у悗鍙戠幇浜у搧琛ㄩ潰鏈夊垝鐥�")
+ private String issueDescription;
+
+ @Schema(description = "涓ラ噸绋嬪害: 0-杞诲井 1-涓�鑸� 2-涓ラ噸 3-鑷村懡", example = "1")
+ private Integer severityLevel;
+
+ @Schema(description = "鐘舵��: 0-鑽夌 10-澶勭悊涓� 30-寰呴��璐� 40-寰呴��娆�/鎹㈣揣 50-宸插畬缁� 60-宸插叧闂� 99-宸插彇娑�", example = "0")
+ private Integer status;
+
+ @Schema(description = "璐熻矗浜虹敤鎴风紪鍙�", example = "1")
+ private Long ownerUserId;
+
+ @Schema(description = "璐熻矗浜哄悕绉�")
+ private String ownerUserName;
+
+ @Schema(description = "澶勭悊浜虹敤鎴风紪鍙�", example = "2")
+ private Long handlerUserId;
+
+ @Schema(description = "澶勭悊浜哄悕绉�")
+ private String handlerUserName;
+
+ @Schema(description = "BPM 宸ヤ綔娴佸疄渚嬬紪鍙�")
+ private String processInstanceId;
+
+ @Schema(description = "鍏宠仈閫�璐х敵璇风紪鍙�", example = "1")
+ private Long returnId;
+
+ @Schema(description = "鍏宠仈缁翠慨璁板綍缂栧彿", example = "1")
+ private Long repairId;
+
+ @Schema(description = "瑙e喅鏃堕棿")
+ private LocalDateTime resolvedTime;
+
+ @Schema(description = "鍏抽棴鏃堕棿")
+ private LocalDateTime closedTime;
+
+ @Schema(description = "澶囨敞")
+ private String remark;
+
+ @Schema(description = "鍒涘缓鑰�")
+ private String creator;
+
+ @Schema(description = "鍒涘缓鏃堕棿")
+ private LocalDateTime createTime;
+
+ @Schema(description = "鏇存柊鑰�")
+ private String updater;
+
+ @Schema(description = "鏇存柊鏃堕棿")
+ private LocalDateTime updateTime;
+
+ @Schema(description = "鏄庣粏琛屽垪琛�")
+ private List<Item> items;
+
+ @Schema(description = "宸ュ崟鏄庣粏琛�")
+ @Data
+ public static class Item {
+
+ @Schema(description = "缂栧彿", example = "1")
+ private Long id;
+
+ @Schema(description = "宸ュ崟缂栧彿", example = "1")
+ private Long ticketId;
+
+ @Schema(description = "閿�鍞鍗曡缂栧彿", example = "1")
+ private Long saleOrderItemId;
+
+ @Schema(description = "MDM鐗╂枡缂栧彿", example = "1")
+ private Long itemId;
+
+ @Schema(description = "鐗╂枡缂栫爜", example = "MAT-001")
+ private String itemCode;
+
+ @Schema(description = "鐗╂枡鍚嶇О", example = "鐢靛瓙鍏冨櫒浠�")
+ private String itemName;
+
+ @Schema(description = "鐢熶骇鎵规缂栧彿", example = "1")
+ private Long batchId;
+
+ @Schema(description = "鐢熶骇鎵规鍙�", example = "BATCH-2024-001")
+ private String batchCode;
+
+ @Schema(description = "閿�鍞暟閲�", example = "100.000")
+ private BigDecimal saleQuantity;
+
+ @Schema(description = "閫�璐ф暟閲�", example = "10.000")
+ private BigDecimal returnQuantity;
+
+ @Schema(description = "鏄惁闇�瑕佺淮淇�", example = "false")
+ private Boolean needRepair;
+
+ @Schema(description = "鏄惁闇�瑕侀��璐�", example = "true")
+ private Boolean needReturn;
+
+ @Schema(description = "澶囨敞")
+ private String remark;
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSaveReqVO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSaveReqVO.java
new file mode 100644
index 0000000..c19012e
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSaveReqVO.java
@@ -0,0 +1,89 @@
+package cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 鍞悗宸ュ崟淇濆瓨 VO
+ */
+@Schema(description = "鍞悗宸ュ崟 - 淇濆瓨 Request VO")
+@Data
+public class AfterSaleTicketSaveReqVO {
+
+ @Schema(description = "宸ュ崟缂栧彿", example = "1")
+ private Long id;
+
+ @Schema(description = "宸ュ崟涓婚", requiredMode = Schema.RequiredMode.REQUIRED, example = "浜у搧澶栬缂洪櫡")
+ @NotEmpty(message = "宸ュ崟涓婚涓嶈兘涓虹┖")
+ private String name;
+
+ @Schema(description = "瀹㈡埛缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "瀹㈡埛涓嶈兘涓虹┖")
+ private Long customerId;
+
+ @Schema(description = "鑱旂郴浜虹紪鍙�", example = "1")
+ private Long contactId;
+
+ @Schema(description = "閿�鍞鍗曠紪鍙�", example = "1")
+ private Long saleOrderId;
+
+ @Schema(description = "閿�鍞鍗曞彿", example = "SO202401010001")
+ private String saleOrderNo;
+
+ @Schema(description = "鍚堝悓缂栧彿", example = "1")
+ private Long contractId;
+
+ @Schema(description = "闂鎻忚堪", example = "鏀跺埌璐у悗鍙戠幇浜у搧琛ㄩ潰鏈夊垝鐥�")
+ private String issueDescription;
+
+ @Schema(description = "澶囨敞", example = "瀹㈡埛瑕佹眰灏藉揩澶勭悊")
+ private String remark;
+
+ @Schema(description = "鏄庣粏琛屽垪琛�")
+ private List<Item> items;
+
+ @Schema(description = "宸ュ崟鏄庣粏琛�")
+ @Data
+ public static class Item {
+
+ @Schema(description = "閿�鍞鍗曡缂栧彿", example = "1")
+ private Long saleOrderItemId;
+
+ @Schema(description = "MDM鐗╂枡缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotNull(message = "鐗╂枡涓嶈兘涓虹┖")
+ private Long itemId;
+
+ @Schema(description = "鐗╂枡缂栫爜", example = "MAT-001")
+ private String itemCode;
+
+ @Schema(description = "鐗╂枡鍚嶇О", example = "鐢靛瓙鍏冨櫒浠�")
+ private String itemName;
+
+ @Schema(description = "鐢熶骇鎵规缂栧彿", example = "1")
+ private Long batchId;
+
+ @Schema(description = "鐢熶骇鎵规鍙�", example = "BATCH-2024-001")
+ private String batchCode;
+
+ @Schema(description = "閿�鍞暟閲�", example = "100.000")
+ private BigDecimal saleQuantity;
+
+ @Schema(description = "閫�璐ф暟閲�", example = "10.000")
+ private BigDecimal returnQuantity;
+
+ @Schema(description = "鏄惁闇�瑕佺淮淇�", example = "false")
+ private Boolean needRepair;
+
+ @Schema(description = "鏄惁闇�瑕侀��璐�", example = "true")
+ private Boolean needReturn;
+
+ @Schema(description = "澶囨敞")
+ private String remark;
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/permission/AfterSalePermissionDO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/permission/AfterSalePermissionDO.java
new file mode 100644
index 0000000..d77e2ef
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/permission/AfterSalePermissionDO.java
@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.module.aftersales.dal.dataobject.permission;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
+import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 鍞悗妯″潡鏁版嵁鏉冮檺 DO
+ */
+@TableName("after_sale_permission")
+@KeySequence("after_sale_permission_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AfterSalePermissionDO extends BaseDO {
+
+ /**
+ * 缂栧彿锛屼富閿嚜澧�
+ */
+ @TableId
+ private Long id;
+
+ /**
+ * 鏁版嵁绫诲瀷
+ *
+ * 鏋氫妇 {@link AfterSaleBizTypeEnum}
+ */
+ private Integer bizType;
+ /**
+ * 鏁版嵁缂栧彿
+ *
+ * 鍏宠仈 {@link AfterSaleBizTypeEnum} 瀵瑰簲妯″潡 DO 鐨� id 瀛楁
+ */
+ private Long bizId;
+
+ /**
+ * 鐢ㄦ埛缂栧彿
+ */
+ private Long userId;
+
+ /**
+ * 鏉冮檺绾у埆
+ *
+ * 鏋氫妇 {@link AfterSalePermissionLevelEnum}
+ */
+ private Integer level;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/repair/AfterSaleRepairDO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/repair/AfterSaleRepairDO.java
new file mode 100644
index 0000000..8ecb859
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/repair/AfterSaleRepairDO.java
@@ -0,0 +1,78 @@
+package cn.iocoder.yudao.module.aftersales.dal.dataobject.repair;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleRepairStatusEnum;
+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
+ */
+@TableName("after_sale_repair")
+@KeySequence("after_sale_repair_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AfterSaleRepairDO extends BaseDO {
+
+ /**
+ * 缁翠慨璁板綍缂栧彿
+ */
+ @TableId
+ private Long id;
+ /**
+ * 鍏宠仈宸ュ崟缂栧彿锛屽叧鑱� after_sale_ticket.id
+ */
+ private Long ticketId;
+ /**
+ * 缁翠慨绫诲瀷: 1-鐜板満缁翠慨 2-杩斿巶缁翠慨 3-杩滅▼鎸囧
+ */
+ private Integer repairType;
+ /**
+ * 鏁呴殰鎻忚堪
+ */
+ private String faultDescription;
+ /**
+ * 缁翠慨鎺柦
+ */
+ private String repairAction;
+ /**
+ * 缁翠慨缁撴灉: 宸蹭慨澶�/鏃犳硶淇/閮ㄥ垎淇
+ */
+ private String repairResult;
+ /**
+ * 缁翠慨璐圭敤
+ */
+ private BigDecimal repairCost;
+ /**
+ * 缁翠慨浜�
+ */
+ private Long repairUserId;
+ /**
+ * 缁翠慨鐘舵��
+ *
+ * 鏋氫妇 {@link AfterSaleRepairStatusEnum}
+ */
+ private Integer status;
+ /**
+ * 寮�濮嬬淮淇椂闂�
+ */
+ private LocalDateTime startTime;
+ /**
+ * 瀹屾垚缁翠慨鏃堕棿
+ */
+ private LocalDateTime finishTime;
+ /**
+ * 澶囨敞
+ */
+ private String remark;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/returnobj/AfterSaleReturnDO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/returnobj/AfterSaleReturnDO.java
new file mode 100644
index 0000000..776db7f
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/returnobj/AfterSaleReturnDO.java
@@ -0,0 +1,112 @@
+package cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj;
+
+// 娉細package 鍚嶄娇鐢� returnobj 閬垮厤涓� Java 鍏抽敭瀛� return 鍐茬獊
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleReturnStatusEnum;
+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
+ */
+@TableName("after_sale_return")
+@KeySequence("after_sale_return_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AfterSaleReturnDO extends BaseDO {
+
+ /**
+ * 閫�璐х敵璇风紪鍙�
+ */
+ @TableId
+ private Long id;
+ /**
+ * 閫�璐у崟鍙�
+ */
+ private String no;
+ /**
+ * 鍏宠仈宸ュ崟缂栧彿锛屽叧鑱� after_sale_ticket.id
+ */
+ private Long ticketId;
+ /**
+ * 瀹㈡埛缂栧彿
+ */
+ private Long customerId;
+ /**
+ * 鍏宠仈閿�鍞鍗曠紪鍙�
+ */
+ private Long saleOrderId;
+ /**
+ * 閫�璐х被鍨�: 1-閫�娆鹃��璐� 2-鎹㈣揣 3-浠呴��娆�
+ */
+ private Integer returnType;
+ /**
+ * 閫�璐у師鍥�
+ */
+ private String returnReason;
+ /**
+ * 缂洪櫡鍒嗙被
+ */
+ private String defectCategory;
+ /**
+ * 搴旈��鎬婚噾棰�
+ */
+ private BigDecimal totalReturnPrice;
+ /**
+ * 瀹為��閲戦
+ */
+ private BigDecimal actualRefundPrice;
+ /**
+ * 閫�璐х姸鎬�
+ *
+ * 鏋氫妇 {@link AfterSaleReturnStatusEnum}
+ */
+ private Integer status;
+ /**
+ * 瀹℃牳浜�
+ */
+ private Long auditUserId;
+ /**
+ * 瀹℃牳鏃堕棿
+ */
+ private LocalDateTime auditTime;
+ /**
+ * MES 閫�璐у崟缂栧彿锛屽叧鑱� mes_wm_return_sales.id
+ */
+ private Long mesReturnId;
+ /**
+ * MES 閫�璐у崟鍙�(鍐椾綑)
+ */
+ private String mesReturnCode;
+ /**
+ * 璐ㄦ缁撹: 鍚堟牸/鐗归噰/閫�璐�/鎶ュ簾
+ */
+ private String qualityResult;
+ /**
+ * 璐ㄦ鎶ュ憡闄勪欢URL
+ */
+ private String qualityReportUrl;
+ /**
+ * 閫�璐у叆搴撳畬鎴愭椂闂�
+ */
+ private LocalDateTime returnInboundTime;
+ /**
+ * 閫�娆�/鎹㈣揣瀹屾垚鏃堕棿
+ */
+ private LocalDateTime refundTime;
+ /**
+ * 澶囨敞
+ */
+ private String remark;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/returnobj/AfterSaleReturnItemDO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/returnobj/AfterSaleReturnItemDO.java
new file mode 100644
index 0000000..53bc524
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/returnobj/AfterSaleReturnItemDO.java
@@ -0,0 +1,78 @@
+package cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+
+/**
+ * 鍞悗閫�璐ф槑缁嗚 DO
+ */
+@TableName("after_sale_return_item")
+@KeySequence("after_sale_return_item_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AfterSaleReturnItemDO extends BaseDO {
+
+ /**
+ * 缂栧彿
+ */
+ @TableId
+ private Long id;
+ /**
+ * 閫�璐х敵璇风紪鍙凤紝鍏宠仈 after_sale_return.id
+ */
+ private Long returnId;
+ /**
+ * 宸ュ崟鏄庣粏琛岀紪鍙�
+ */
+ private Long ticketItemId;
+ /**
+ * MDM 鐗╂枡缂栧彿
+ */
+ private Long itemId;
+ /**
+ * 鐗╂枡缂栫爜(鍐椾綑)
+ */
+ private String itemCode;
+ /**
+ * 鐗╂枡鍚嶇О(鍐椾綑)
+ */
+ private String itemName;
+ /**
+ * 鎵规缂栧彿
+ */
+ private Long batchId;
+ /**
+ * 鎵规鍙�
+ */
+ private String batchCode;
+ /**
+ * 閫�璐ф暟閲�
+ */
+ private BigDecimal returnQuantity;
+ /**
+ * 閿�鍞崟浠�
+ */
+ private BigDecimal unitPrice;
+ /**
+ * 閫�璐ч噾棰�
+ */
+ private BigDecimal returnPrice;
+ /**
+ * MES 閫�璐у崟琛岀紪鍙�
+ */
+ private Long mesReturnLineId;
+ /**
+ * 澶囨敞
+ */
+ private String remark;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketDO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketDO.java
new file mode 100644
index 0000000..a2eee67
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketDO.java
@@ -0,0 +1,116 @@
+package cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleIssueTypeEnum;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketStatusEnum;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * 鍞悗宸ュ崟 DO
+ */
+@TableName("after_sale_ticket")
+@KeySequence("after_sale_ticket_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AfterSaleTicketDO extends BaseDO {
+
+ /**
+ * 宸ュ崟缂栧彿
+ */
+ @TableId
+ private Long id;
+ /**
+ * 宸ュ崟鍗曞彿
+ */
+ private String no;
+ /**
+ * 宸ュ崟涓婚/鏍囬
+ */
+ private String name;
+ /**
+ * 瀹㈡埛缂栧彿锛屽叧鑱� crm_customer.id
+ */
+ private Long customerId;
+ /**
+ * 鑱旂郴浜虹紪鍙凤紝鍏宠仈 crm_contact.id
+ */
+ private Long contactId;
+ /**
+ * 閿�鍞鍗曠紪鍙凤紝鍏宠仈 erp_sale_order.id
+ */
+ private Long saleOrderId;
+ /**
+ * 閿�鍞鍗曞彿(鍐椾綑)
+ */
+ private String saleOrderNo;
+ /**
+ * 鍚堝悓缂栧彿锛屽叧鑱� crm_contract.id
+ */
+ private Long contractId;
+ /**
+ * 闂绫诲瀷
+ *
+ * 鏋氫妇 {@link AfterSaleIssueTypeEnum}
+ */
+ private Integer issueType;
+ /**
+ * 闂鍒嗙被: 澶栬缂洪櫡/鍔熻兘鏁呴殰/灏哄鍋忓樊/鍖呰鐮存崯/鍏朵粬
+ */
+ private String issueCategory;
+ /**
+ * 闂鎻忚堪
+ */
+ private String issueDescription;
+ /**
+ * 涓ラ噸绋嬪害: 0-杞诲井 1-涓�鑸� 2-涓ラ噸 3-鑷村懡
+ */
+ private Integer severityLevel;
+ /**
+ * 宸ュ崟鐘舵��
+ *
+ * 鏋氫妇 {@link AfterSaleTicketStatusEnum}
+ */
+ private Integer status;
+ /**
+ * 璐熻矗浜虹敤鎴风紪鍙�
+ */
+ private Long ownerUserId;
+ /**
+ * 澶勭悊浜虹敤鎴风紪鍙�
+ */
+ private Long handlerUserId;
+ /**
+ * BPM 宸ヤ綔娴佸疄渚嬬紪鍙�
+ */
+ private String processInstanceId;
+ /**
+ * 鍏宠仈閫�璐х敵璇风紪鍙凤紝鍏宠仈 after_sale_return.id
+ */
+ private Long returnId;
+ /**
+ * 鍏宠仈缁翠慨璁板綍缂栧彿锛屽叧鑱� after_sale_repair.id
+ */
+ private Long repairId;
+ /**
+ * 瑙e喅鏃堕棿
+ */
+ private LocalDateTime resolvedTime;
+ /**
+ * 鍏抽棴鏃堕棿
+ */
+ private LocalDateTime closedTime;
+ /**
+ * 澶囨敞
+ */
+ private String remark;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketItemDO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketItemDO.java
new file mode 100644
index 0000000..28af429
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/dataobject/ticket/AfterSaleTicketItemDO.java
@@ -0,0 +1,78 @@
+package cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+
+/**
+ * 鍞悗宸ュ崟鏄庣粏琛� DO
+ */
+@TableName("after_sale_ticket_item")
+@KeySequence("after_sale_ticket_item_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AfterSaleTicketItemDO extends BaseDO {
+
+ /**
+ * 缂栧彿
+ */
+ @TableId
+ private Long id;
+ /**
+ * 宸ュ崟缂栧彿锛屽叧鑱� after_sale_ticket.id
+ */
+ private Long ticketId;
+ /**
+ * 閿�鍞鍗曡缂栧彿
+ */
+ private Long saleOrderItemId;
+ /**
+ * MDM 鐗╂枡缂栧彿
+ */
+ private Long itemId;
+ /**
+ * 鐗╂枡缂栫爜(鍐椾綑)
+ */
+ private String itemCode;
+ /**
+ * 鐗╂枡鍚嶇О(鍐椾綑)
+ */
+ private String itemName;
+ /**
+ * 鐢熶骇鎵规缂栧彿
+ */
+ private Long batchId;
+ /**
+ * 鐢熶骇鎵规鍙�
+ */
+ private String batchCode;
+ /**
+ * 閿�鍞暟閲�
+ */
+ private BigDecimal saleQuantity;
+ /**
+ * 閫�璐ф暟閲�
+ */
+ private BigDecimal returnQuantity;
+ /**
+ * 鏄惁闇�瑕佺淮淇�
+ */
+ private Boolean needRepair;
+ /**
+ * 鏄惁闇�瑕侀��璐�
+ */
+ private Boolean needReturn;
+ /**
+ * 澶囨敞
+ */
+ private String remark;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/permission/AfterSalePermissionMapper.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/permission/AfterSalePermissionMapper.java
new file mode 100644
index 0000000..8daa058
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/permission/AfterSalePermissionMapper.java
@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.module.aftersales.dal.mysql.permission;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.permission.AfterSalePermissionDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 鍞悗妯″潡鏁版嵁鏉冮檺 Mapper
+ */
+@Mapper
+public interface AfterSalePermissionMapper extends BaseMapperX<AfterSalePermissionDO> {
+
+ default List<AfterSalePermissionDO> selectListByBiz(Integer bizType, Long bizId) {
+ return selectList(new LambdaQueryWrapperX<AfterSalePermissionDO>()
+ .eq(AfterSalePermissionDO::getBizType, bizType)
+ .eq(AfterSalePermissionDO::getBizId, bizId));
+ }
+
+ default List<AfterSalePermissionDO> selectListByBiz(Integer bizType, Collection<Long> bizIds) {
+ return selectList(new LambdaQueryWrapperX<AfterSalePermissionDO>()
+ .eq(AfterSalePermissionDO::getBizType, bizType)
+ .in(AfterSalePermissionDO::getBizId, bizIds));
+ }
+
+ default List<AfterSalePermissionDO> selectListByBizTypeAndUserId(Integer bizType, Long userId) {
+ return selectList(new LambdaQueryWrapperX<AfterSalePermissionDO>()
+ .eq(AfterSalePermissionDO::getBizType, bizType)
+ .eq(AfterSalePermissionDO::getUserId, userId));
+ }
+
+ default AfterSalePermissionDO selectByBizAndUser(Integer bizType, Long bizId, Long userId) {
+ return selectOne(new LambdaQueryWrapperX<AfterSalePermissionDO>()
+ .eq(AfterSalePermissionDO::getBizType, bizType)
+ .eq(AfterSalePermissionDO::getBizId, bizId)
+ .eq(AfterSalePermissionDO::getUserId, userId));
+ }
+
+ default void deleteByBiz(Integer bizType, Long bizId) {
+ delete(new LambdaQueryWrapperX<AfterSalePermissionDO>()
+ .eq(AfterSalePermissionDO::getBizType, bizType)
+ .eq(AfterSalePermissionDO::getBizId, bizId));
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/repair/AfterSaleRepairMapper.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/repair/AfterSaleRepairMapper.java
new file mode 100644
index 0000000..3ac344c
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/repair/AfterSaleRepairMapper.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.aftersales.dal.mysql.repair;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.repair.AfterSaleRepairDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 鍞悗缁翠慨璁板綍 Mapper
+ */
+@Mapper
+public interface AfterSaleRepairMapper extends BaseMapperX<AfterSaleRepairDO> {
+
+ default AfterSaleRepairDO selectByTicketId(Long ticketId) {
+ return selectOne(AfterSaleRepairDO::getTicketId, ticketId);
+ }
+
+ default List<AfterSaleRepairDO> selectListByTicketId(Long ticketId) {
+ return selectList(new LambdaQueryWrapperX<AfterSaleRepairDO>()
+ .eq(AfterSaleRepairDO::getTicketId, ticketId));
+ }
+
+ default PageResult<AfterSaleRepairDO> selectPage(PageParam pageParam, Long ticketId, Integer status) {
+ return selectPage(pageParam, new LambdaQueryWrapperX<AfterSaleRepairDO>()
+ .eqIfPresent(AfterSaleRepairDO::getTicketId, ticketId)
+ .eqIfPresent(AfterSaleRepairDO::getStatus, status)
+ .orderByDesc(AfterSaleRepairDO::getId));
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/returnobj/AfterSaleReturnItemMapper.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/returnobj/AfterSaleReturnItemMapper.java
new file mode 100644
index 0000000..d9d9ece
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/returnobj/AfterSaleReturnItemMapper.java
@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.aftersales.dal.mysql.returnobj;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnItemDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 鍞悗閫�璐ф槑缁嗚 Mapper
+ */
+@Mapper
+public interface AfterSaleReturnItemMapper extends BaseMapperX<AfterSaleReturnItemDO> {
+
+ default List<AfterSaleReturnItemDO> selectListByReturnId(Long returnId) {
+ return selectList(new LambdaQueryWrapperX<AfterSaleReturnItemDO>()
+ .eq(AfterSaleReturnItemDO::getReturnId, returnId));
+ }
+
+ default void deleteByReturnId(Long returnId) {
+ delete(new LambdaQueryWrapperX<AfterSaleReturnItemDO>()
+ .eq(AfterSaleReturnItemDO::getReturnId, returnId));
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/returnobj/AfterSaleReturnMapper.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/returnobj/AfterSaleReturnMapper.java
new file mode 100644
index 0000000..eb0ac14
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/returnobj/AfterSaleReturnMapper.java
@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.module.aftersales.dal.mysql.returnobj;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+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.aftersales.dal.dataobject.returnobj.AfterSaleReturnDO;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
+import cn.iocoder.yudao.module.aftersales.util.AfterSalePermissionUtils;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 鍞悗閫�璐х敵璇� Mapper
+ */
+@Mapper
+public interface AfterSaleReturnMapper extends BaseMapperX<AfterSaleReturnDO> {
+
+ default AfterSaleReturnDO selectByNo(String no) {
+ return selectOne(AfterSaleReturnDO::getNo, no);
+ }
+
+ default AfterSaleReturnDO selectByTicketId(Long ticketId) {
+ return selectOne(AfterSaleReturnDO::getTicketId, ticketId);
+ }
+
+ default PageResult<AfterSaleReturnDO> selectPage(PageParam pageParam, Long userId, Integer sceneType,
+ String no, Long ticketId,
+ Long customerId, Integer status) {
+ MPJLambdaWrapperX<AfterSaleReturnDO> query = new MPJLambdaWrapperX<>();
+ // 鏁版嵁鏉冮檺
+ AfterSalePermissionUtils.appendPermissionCondition(query, AfterSaleBizTypeEnum.AFTER_SALE_RETURN.getType(),
+ AfterSaleReturnDO::getId, userId, sceneType);
+ // 鏌ヨ鏉′欢
+ query.selectAll(AfterSaleReturnDO.class)
+ .likeIfPresent(AfterSaleReturnDO::getNo, no)
+ .eqIfPresent(AfterSaleReturnDO::getTicketId, ticketId)
+ .eqIfPresent(AfterSaleReturnDO::getCustomerId, customerId)
+ .eqIfPresent(AfterSaleReturnDO::getStatus, status)
+ .orderByDesc(AfterSaleReturnDO::getId);
+ return selectJoinPage(pageParam, AfterSaleReturnDO.class, query);
+ }
+
+ default AfterSaleReturnDO selectByMesReturnId(Long mesReturnId) {
+ return selectOne(AfterSaleReturnDO::getMesReturnId, mesReturnId);
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketItemMapper.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketItemMapper.java
new file mode 100644
index 0000000..ea609b9
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketItemMapper.java
@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.aftersales.dal.mysql.ticket;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketItemDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 鍞悗宸ュ崟鏄庣粏琛� Mapper
+ */
+@Mapper
+public interface AfterSaleTicketItemMapper extends BaseMapperX<AfterSaleTicketItemDO> {
+
+ default List<AfterSaleTicketItemDO> selectListByTicketId(Long ticketId) {
+ return selectList(new LambdaQueryWrapperX<AfterSaleTicketItemDO>()
+ .eq(AfterSaleTicketItemDO::getTicketId, ticketId));
+ }
+
+ default void deleteByTicketId(Long ticketId) {
+ delete(new LambdaQueryWrapperX<AfterSaleTicketItemDO>()
+ .eq(AfterSaleTicketItemDO::getTicketId, ticketId));
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketMapper.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketMapper.java
new file mode 100644
index 0000000..2546020
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/ticket/AfterSaleTicketMapper.java
@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.aftersales.dal.mysql.ticket;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+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.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
+import cn.iocoder.yudao.module.aftersales.util.AfterSalePermissionUtils;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 鍞悗宸ュ崟 Mapper
+ */
+@Mapper
+public interface AfterSaleTicketMapper extends BaseMapperX<AfterSaleTicketDO> {
+
+ default AfterSaleTicketDO selectByNo(String no) {
+ return selectOne(AfterSaleTicketDO::getNo, no);
+ }
+
+ default PageResult<AfterSaleTicketDO> selectPage(PageParam pageParam, Long userId, Integer sceneType,
+ String no, String name,
+ Long customerId, Long saleOrderId,
+ Integer status, Integer issueType) {
+ MPJLambdaWrapperX<AfterSaleTicketDO> query = new MPJLambdaWrapperX<>();
+ // 鏁版嵁鏉冮檺
+ AfterSalePermissionUtils.appendPermissionCondition(query, AfterSaleBizTypeEnum.AFTER_SALE_TICKET.getType(),
+ AfterSaleTicketDO::getId, userId, sceneType);
+ // 鏌ヨ鏉′欢
+ query.selectAll(AfterSaleTicketDO.class)
+ .likeIfPresent(AfterSaleTicketDO::getNo, no)
+ .likeIfPresent(AfterSaleTicketDO::getName, name)
+ .eqIfPresent(AfterSaleTicketDO::getCustomerId, customerId)
+ .eqIfPresent(AfterSaleTicketDO::getSaleOrderId, saleOrderId)
+ .eqIfPresent(AfterSaleTicketDO::getStatus, status)
+ .eqIfPresent(AfterSaleTicketDO::getIssueType, issueType)
+ .orderByDesc(AfterSaleTicketDO::getId);
+ return selectJoinPage(pageParam, AfterSaleTicketDO.class, query);
+ }
+
+ default List<AfterSaleTicketDO> selectListByCustomerId(Long customerId) {
+ return selectList(new LambdaQueryWrapperX<AfterSaleTicketDO>()
+ .eq(AfterSaleTicketDO::getCustomerId, customerId));
+ }
+
+ default AfterSaleTicketDO selectBySaleOrderId(Long saleOrderId) {
+ return selectOne(AfterSaleTicketDO::getSaleOrderId, saleOrderId);
+ }
+
+ default Long selectCountByOwnerUserId(Long ownerUserId) {
+ return selectCount(AfterSaleTicketDO::getOwnerUserId, ownerUserId);
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/RedisKeyConstants.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/RedisKeyConstants.java
new file mode 100644
index 0000000..575dde6
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/RedisKeyConstants.java
@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.aftersales.dal.redis;
+
+/**
+ * 鍞悗妯″潡 Redis Key 甯搁噺
+ */
+public interface RedisKeyConstants {
+
+ /**
+ * 搴忓彿鐨勭紦瀛�
+ *
+ * KEY 鏍煎紡锛歛ftersales:seq_no:{prefix}
+ * VALUE 鏁版嵁鏍煎紡锛氱紪鍙疯嚜澧�
+ */
+ String NO = "aftersales:seq_no:";
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java
new file mode 100644
index 0000000..856c862
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java
@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.aftersales.dal.redis.no;
+
+import cn.hutool.core.date.DatePattern;
+import cn.hutool.core.date.DateUtil;
+import cn.iocoder.yudao.module.aftersales.dal.redis.RedisKeyConstants;
+import jakarta.annotation.Resource;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+
+/**
+ * 鍞悗妯″潡鍗曞彿 Redis DAO
+ */
+@Repository
+public class AfterSaleNoRedisDAO {
+
+ /**
+ * 鍞悗宸ュ崟 {@link cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO}
+ */
+ public static final String TICKET_NO_PREFIX = "AS";
+
+ /**
+ * 閫�璐х敵璇� {@link cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnDO}
+ */
+ public static final String RETURN_NO_PREFIX = "RT";
+
+ @Resource
+ private StringRedisTemplate stringRedisTemplate;
+
+ /**
+ * 鐢熸垚搴忓彿锛屼娇鐢ㄥ綋鍓嶆棩鏈燂紝鏍煎紡涓� {PREFIX} + yyyyMMdd + 6 浣嶈嚜澧�
+ *
+ * @param prefix 鍓嶇紑
+ * @return 搴忓彿
+ */
+ public String generate(String prefix) {
+ String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
+ String key = RedisKeyConstants.NO + noPrefix;
+ Long no = stringRedisTemplate.opsForValue().increment(key);
+ stringRedisTemplate.expire(key, Duration.ofDays(1L));
+ return noPrefix + String.format("%06d", no);
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleBizTypeEnum.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleBizTypeEnum.java
new file mode 100644
index 0000000..5408b5a
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleBizTypeEnum.java
@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.aftersales.enums;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 鍞悗涓氬姟绫诲瀷鏋氫妇
+ */
+@RequiredArgsConstructor
+@Getter
+public enum AfterSaleBizTypeEnum implements ArrayValuable<Integer> {
+
+ AFTER_SALE_TICKET(1, "鍞悗宸ュ崟"),
+ AFTER_SALE_RETURN(2, "閫�璐х敵璇�"),
+ AFTER_SALE_REPAIR(3, "缁翠慨璁板綍"),
+ ;
+
+ public static final Integer[] ARRAYS = Arrays.stream(values())
+ .map(AfterSaleBizTypeEnum::getType).toArray(Integer[]::new);
+
+ private final Integer type;
+ private final String name;
+
+ public static String getNameByType(Integer type) {
+ AfterSaleBizTypeEnum typeEnum = CollUtil.findOne(CollUtil.newArrayList(AfterSaleBizTypeEnum.values()),
+ item -> ObjUtil.equal(item.type, type));
+ return typeEnum == null ? null : typeEnum.getName();
+ }
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleIssueTypeEnum.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleIssueTypeEnum.java
new file mode 100644
index 0000000..ff45d51
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleIssueTypeEnum.java
@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.aftersales.enums;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 鍞悗闂绫诲瀷鏋氫妇
+ */
+@RequiredArgsConstructor
+@Getter
+public enum AfterSaleIssueTypeEnum implements ArrayValuable<Integer> {
+
+ UNCLASSIFIED(0, "鏈垽瀹�"),
+ GENERAL(1, "涓�鑸棶棰�"),
+ REPAIR(2, "缁翠慨闂"),
+ RETURN(3, "閫�璐ч棶棰�");
+
+ public static final Integer[] ARRAYS = Arrays.stream(values())
+ .map(AfterSaleIssueTypeEnum::getType).toArray(Integer[]::new);
+
+ private final Integer type;
+ private final String name;
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleRepairStatusEnum.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleRepairStatusEnum.java
new file mode 100644
index 0000000..33a8cc8
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleRepairStatusEnum.java
@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.aftersales.enums;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 鍞悗缁翠慨鐘舵�佹灇涓�
+ */
+@RequiredArgsConstructor
+@Getter
+public enum AfterSaleRepairStatusEnum implements ArrayValuable<Integer> {
+
+ PENDING(0, "寰呭鐞�"),
+ IN_PROGRESS(10, "缁翠慨涓�"),
+ COMPLETED(20, "宸插畬鎴�"),
+ CLOSED(30, "宸插叧闂�");
+
+ public static final Integer[] ARRAYS = Arrays.stream(values())
+ .map(AfterSaleRepairStatusEnum::getStatus).toArray(Integer[]::new);
+
+ private final Integer status;
+ private final String name;
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleReturnStatusEnum.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleReturnStatusEnum.java
new file mode 100644
index 0000000..634b540
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleReturnStatusEnum.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.aftersales.enums;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 鍞悗閫�璐х姸鎬佹灇涓�
+ */
+@RequiredArgsConstructor
+@Getter
+public enum AfterSaleReturnStatusEnum implements ArrayValuable<Integer> {
+
+ DRAFT(0, "鑽夌"),
+ AUDITING(10, "閿�鍞鏍镐腑"),
+ APPROVED(20, "瀹℃牳閫氳繃-宸插悓姝ES"),
+ INSPECTING(30, "妫�楠屼腑"),
+ INSPECTED(40, "妫�楠屽畬鎴�"),
+ INBOUND(50, "宸插叆搴�"),
+ REFUNDED(60, "閫�娆�/鎹㈣揣瀹屾垚"),
+ CLOSED(70, "宸插叧闂�"),
+ CANCELED(99, "宸插彇娑�");
+
+ public static final Integer[] ARRAYS = Arrays.stream(values())
+ .map(AfterSaleReturnStatusEnum::getStatus).toArray(Integer[]::new);
+
+ private final Integer status;
+ private final String name;
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleTicketStatusEnum.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleTicketStatusEnum.java
new file mode 100644
index 0000000..88f6da8
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/AfterSaleTicketStatusEnum.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.aftersales.enums;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 鍞悗宸ュ崟鐘舵�佹灇涓�
+ */
+@RequiredArgsConstructor
+@Getter
+public enum AfterSaleTicketStatusEnum implements ArrayValuable<Integer> {
+
+ DRAFT(0, "鑽夌"),
+ PROCESSING(10, "澶勭悊涓�"),
+ WAITING_RETURN(30, "寰呴��璐�"),
+ WAITING_REFUND(40, "寰呴��娆�/鎹㈣揣"),
+ COMPLETED(50, "宸插畬缁�"),
+ CLOSED(60, "宸插叧闂�"),
+ CANCELED(99, "宸插彇娑�");
+
+ public static final Integer[] ARRAYS = Arrays.stream(values())
+ .map(AfterSaleTicketStatusEnum::getStatus).toArray(Integer[]::new);
+
+ private final Integer status;
+ private final String name;
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/ErrorCodeConstants.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/ErrorCodeConstants.java
new file mode 100644
index 0000000..1796711
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/ErrorCodeConstants.java
@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.aftersales.enums;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+
+/**
+ * 鍞悗妯″潡閿欒鐮佹灇涓剧被
+ * <p>
+ * aftersales 绯荤粺锛屼娇鐢� 1-022-000-000 娈�
+ */
+public interface ErrorCodeConstants {
+
+ // ========== 鍞悗宸ュ崟 1-022-000-000 ==========
+ ErrorCode TICKET_NOT_EXISTS = new ErrorCode(1_022_000_000, "鍞悗宸ュ崟涓嶅瓨鍦�");
+ ErrorCode TICKET_NO_EXISTS = new ErrorCode(1_022_000_001, "鐢熸垚宸ュ崟鍙烽噸澶嶏紝璇烽噸璇�");
+ ErrorCode TICKET_DELETE_FAIL_NOT_DRAFT = new ErrorCode(1_022_000_002, "宸ュ崟涓嶆槸鑽夌鐘舵�侊紝鏃犳硶鍒犻櫎");
+ ErrorCode TICKET_UPDATE_FAIL_STATUS = new ErrorCode(1_022_000_003, "鏇存柊宸ュ崟鐘舵�佸け璐ワ紝鍘熷洜锛氬凡缁忔槸璇ョ姸鎬�");
+ ErrorCode TICKET_RESOLVE_FAIL = new ErrorCode(1_022_000_004, "瀹岀粨宸ュ崟澶辫触锛屽師鍥狅細宸ュ崟涓嶆槸澶勭悊涓姸鎬�");
+ ErrorCode TICKET_CLOSE_FAIL = new ErrorCode(1_022_000_005, "鍏抽棴宸ュ崟澶辫触锛屽師鍥狅細宸ュ崟鏈畬缁�");
+ ErrorCode TICKET_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_022_000_006, "鎻愪氦宸ュ崟瀹℃壒澶辫触锛屽師鍥狅細宸ュ崟涓嶆槸鑽夌鐘舵��");
+ ErrorCode TICKET_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_022_000_007, "鍞悗宸ュ崟瀹℃壒娴佺▼涓嶅瓨鍦ㄦ垨鏈縺娲伙紝璇锋鏌� BPM 閰嶇疆");
+ ErrorCode TICKET_STATUS_ERROR = new ErrorCode(1_022_000_008, "宸ュ崟褰撳墠鐘舵�佷笉鍏佽姝ゆ搷浣�");
+
+ // ========== 鍞悗宸ュ崟鏄庣粏琛� 1-022-001-000 ==========
+ ErrorCode TICKET_ITEM_NOT_EXISTS = new ErrorCode(1_022_001_000, "宸ュ崟鏄庣粏琛屼笉瀛樺湪");
+ ErrorCode TICKET_ITEM_EMPTY = new ErrorCode(1_022_001_001, "宸ュ崟鏄庣粏琛屼笉鑳戒负绌�");
+
+ // ========== 閫�璐х敵璇� 1-022-002-000 ==========
+ ErrorCode RETURN_NOT_EXISTS = new ErrorCode(1_022_002_000, "閫�璐х敵璇蜂笉瀛樺湪");
+ ErrorCode RETURN_NO_EXISTS = new ErrorCode(1_022_002_001, "鐢熸垚閫�璐у崟鍙烽噸澶嶏紝璇烽噸璇�");
+ ErrorCode RETURN_APPROVE_FAIL = new ErrorCode(1_022_002_002, "瀹℃牳閫�璐х敵璇峰け璐ワ紝鍘熷洜锛氫笉鏄緟瀹℃牳鐘舵��");
+ ErrorCode RETURN_ITEM_EMPTY = new ErrorCode(1_022_002_003, "閫�璐ф槑缁嗚涓嶈兘涓虹┖");
+ ErrorCode RETURN_DELETE_FAIL_NOT_DRAFT = new ErrorCode(1_022_002_004, "閫�璐х敵璇蜂笉鏄崏绋跨姸鎬侊紝鏃犳硶鍒犻櫎");
+
+ // ========== 缁翠慨璁板綍 1-022-003-000 ==========
+ ErrorCode REPAIR_NOT_EXISTS = new ErrorCode(1_022_003_000, "缁翠慨璁板綍涓嶅瓨鍦�");
+ ErrorCode REPAIR_UPDATE_FAIL_STATUS = new ErrorCode(1_022_003_001, "鏇存柊缁翠慨鐘舵�佸け璐ワ紝鍘熷洜锛氬凡缁忔槸璇ョ姸鎬�");
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/LogRecordConstants.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/LogRecordConstants.java
new file mode 100644
index 0000000..0fd4527
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/LogRecordConstants.java
@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.module.aftersales.enums;
+
+/**
+ * 鍞悗妯″潡鎿嶄綔鏃ュ織甯搁噺
+ */
+public interface LogRecordConstants {
+
+ // ======================= 鍞悗宸ュ崟 =======================
+
+ String AFTER_SALE_TICKET_TYPE = "鍞悗宸ュ崟";
+ String AFTER_SALE_TICKET_CREATE_SUB_TYPE = "鍒涘缓鍞悗宸ュ崟";
+ String AFTER_SALE_TICKET_CREATE_SUCCESS = "鍒涘缓浜嗗敭鍚庡伐鍗曘�恵{#ticket.name}}銆�";
+ String AFTER_SALE_TICKET_UPDATE_SUB_TYPE = "鏇存柊鍞悗宸ュ崟";
+ String AFTER_SALE_TICKET_UPDATE_SUCCESS = "鏇存柊浜嗗敭鍚庡伐鍗曘�恵{#ticketName}}銆�: {_DIFF{#updateReqVO}}";
+ String AFTER_SALE_TICKET_DELETE_SUB_TYPE = "鍒犻櫎鍞悗宸ュ崟";
+ String AFTER_SALE_TICKET_DELETE_SUCCESS = "鍒犻櫎浜嗗敭鍚庡伐鍗曘�恵{#ticketName}}銆�";
+ String AFTER_SALE_TICKET_ASSIGN_SUB_TYPE = "鍒嗛厤澶勭悊浜�";
+ String AFTER_SALE_TICKET_ASSIGN_SUCCESS = "灏嗗敭鍚庡伐鍗曘�恵{#ticketName}}銆戝垎閰嶇粰銆恵getAdminUserById{#handlerUserId}}銆�";
+ String AFTER_SALE_TICKET_CLASSIFY_SUB_TYPE = "闂鍒ゅ畾";
+ String AFTER_SALE_TICKET_CLASSIFY_SUCCESS = "瀵瑰敭鍚庡伐鍗曘�恵{#ticketName}}銆戣繘琛屼簡闂鍒ゅ畾锛氥�恵{#issueTypeName}}銆�";
+ String AFTER_SALE_TICKET_RESOLVE_SUB_TYPE = "瀹岀粨宸ュ崟";
+ String AFTER_SALE_TICKET_RESOLVE_SUCCESS = "瀹岀粨浜嗗敭鍚庡伐鍗曘�恵{#ticketName}}銆�";
+ String AFTER_SALE_TICKET_CLOSE_SUB_TYPE = "鍏抽棴宸ュ崟";
+ String AFTER_SALE_TICKET_CLOSE_SUCCESS = "鍏抽棴浜嗗敭鍚庡伐鍗曘�恵{#ticketName}}銆�";
+ String AFTER_SALE_TICKET_CANCEL_SUB_TYPE = "鍙栨秷宸ュ崟";
+ String AFTER_SALE_TICKET_CANCEL_SUCCESS = "鍙栨秷浜嗗敭鍚庡伐鍗曘�恵{#ticketName}}銆�";
+ String AFTER_SALE_TICKET_SUBMIT_SUB_TYPE = "鎻愪氦宸ュ崟瀹℃壒";
+ String AFTER_SALE_TICKET_SUBMIT_SUCCESS = "鎻愪氦鍞悗宸ュ崟銆恵{#ticketName}}銆戝鎵规垚鍔�";
+
+ // ======================= 閫�璐х敵璇� =======================
+
+ String AFTER_SALE_RETURN_TYPE = "鍞悗閫�璐�";
+ String AFTER_SALE_RETURN_CREATE_SUB_TYPE = "鍒涘缓閫�璐х敵璇�";
+ String AFTER_SALE_RETURN_CREATE_SUCCESS = "鍒涘缓浜嗛��璐х敵璇枫�恵{#returnDO.no}}銆�";
+ String AFTER_SALE_RETURN_APPROVE_SUB_TYPE = "瀹℃牳閫�璐х敵璇�";
+ String AFTER_SALE_RETURN_APPROVE_SUCCESS = "瀹℃牳閫氳繃浜嗛��璐х敵璇枫�恵{#returnNo}}銆戯紝宸插悓姝ュ埌MES閫�璐�";
+ String AFTER_SALE_RETURN_DELETE_SUB_TYPE = "鍒犻櫎閫�璐х敵璇�";
+ String AFTER_SALE_RETURN_DELETE_SUCCESS = "鍒犻櫎浜嗛��璐х敵璇枫�恵{#returnNo}}銆�";
+
+ // ======================= 缁翠慨璁板綍 =======================
+
+ String AFTER_SALE_REPAIR_TYPE = "鍞悗缁翠慨";
+ String AFTER_SALE_REPAIR_CREATE_SUB_TYPE = "鍒涘缓缁翠慨璁板綍";
+ String AFTER_SALE_REPAIR_CREATE_SUCCESS = "鍒涘缓浜嗙淮淇褰曘�恵{#repair.id}}銆�";
+ String AFTER_SALE_REPAIR_COMPLETE_SUB_TYPE = "瀹屾垚缁翠慨";
+ String AFTER_SALE_REPAIR_COMPLETE_SUCCESS = "瀹屾垚浜嗙淮淇褰曘�恵{#repairId}}銆戯紝缁翠慨缁撴灉锛氥�恵{#repairResult}}銆�";
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/permission/AfterSalePermissionLevelEnum.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/permission/AfterSalePermissionLevelEnum.java
new file mode 100644
index 0000000..d086d2d
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/permission/AfterSalePermissionLevelEnum.java
@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.aftersales.enums.permission;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 鍞悗妯″潡鏁版嵁鏉冮檺绾у埆鏋氫妇
+ *
+ * OWNER > WRITE > READ
+ */
+@Getter
+@AllArgsConstructor
+public enum AfterSalePermissionLevelEnum implements ArrayValuable<Integer> {
+
+ OWNER(1, "璐熻矗浜�"),
+ READ(2, "鍙"),
+ WRITE(3, "璇诲啓");
+
+ public static final Integer[] ARRAYS = Arrays.stream(values()).map(AfterSalePermissionLevelEnum::getLevel).toArray(Integer[]::new);
+
+ /**
+ * 绾у埆
+ */
+ private final Integer level;
+ /**
+ * 绾у埆鍚嶇О
+ */
+ private final String name;
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+ public static boolean isOwner(Integer level) {
+ return ObjUtil.equal(OWNER.level, level);
+ }
+
+ public static boolean isRead(Integer level) {
+ return ObjUtil.equal(READ.level, level);
+ }
+
+ public static boolean isWrite(Integer level) {
+ return ObjUtil.equal(WRITE.level, level);
+ }
+
+ public static String getNameByLevel(Integer level) {
+ AfterSalePermissionLevelEnum typeEnum = CollUtil.findOne(CollUtil.newArrayList(AfterSalePermissionLevelEnum.values()),
+ item -> ObjUtil.equal(item.level, level));
+ return typeEnum == null ? null : typeEnum.getName();
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/permission/AfterSaleSceneTypeEnum.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/permission/AfterSaleSceneTypeEnum.java
new file mode 100644
index 0000000..ef371dd
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/permission/AfterSaleSceneTypeEnum.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.aftersales.enums.permission;
+
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 鍞悗妯″潡鍒楄〃妫�绱㈠満鏅�
+ */
+@Getter
+@AllArgsConstructor
+public enum AfterSaleSceneTypeEnum implements ArrayValuable<Integer> {
+
+ OWNER(1, "鎴戣礋璐g殑"),
+ INVOLVED(2, "鎴戝弬涓庣殑"),
+ SUBORDINATE(3, "涓嬪睘璐熻矗鐨�"),
+ ALL(4, "鍏ㄩ儴");
+
+ public static final Integer[] ARRAYS = Arrays.stream(values()).map(AfterSaleSceneTypeEnum::getType).toArray(Integer[]::new);
+
+ /**
+ * 鍦烘櫙绫诲瀷
+ */
+ private final Integer type;
+ /**
+ * 鍦烘櫙鍚嶇О
+ */
+ private final String name;
+
+ public static boolean isOwner(Integer type) {
+ return ObjUtil.equal(OWNER.getType(), type);
+ }
+
+ public static boolean isInvolved(Integer type) {
+ return ObjUtil.equal(INVOLVED.getType(), type);
+ }
+
+ public static boolean isSubordinate(Integer type) {
+ return ObjUtil.equal(SUBORDINATE.getType(), type);
+ }
+
+ public static boolean isAll(Integer type) {
+ return ObjUtil.equal(ALL.getType(), type);
+ }
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/AfterSalePermissionService.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/AfterSalePermissionService.java
new file mode 100644
index 0000000..3426041
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/AfterSalePermissionService.java
@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.aftersales.service.permission;
+
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.permission.AfterSalePermissionDO;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
+import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
+import cn.iocoder.yudao.module.aftersales.service.permission.bo.AfterSalePermissionCreateReqBO;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 鍞悗妯″潡鏁版嵁鏉冮檺 Service 鎺ュ彛
+ */
+public interface AfterSalePermissionService {
+
+ /**
+ * 鍒涘缓鏁版嵁鏉冮檺锛堣礋璐d汉鏉冮檺锛�
+ *
+ * @param createReqBO 鍒涘缓淇℃伅
+ * @return 缂栧彿
+ */
+ Long createPermission(AfterSalePermissionCreateReqBO createReqBO);
+
+ /**
+ * 鍒犻櫎鏁版嵁鏉冮檺
+ *
+ * @param bizType 鏁版嵁绫诲瀷锛屽叧鑱� {@link AfterSaleBizTypeEnum}
+ * @param bizId 鏁版嵁缂栧彿
+ */
+ void deletePermission(Integer bizType, Long bizId);
+
+ /**
+ * 鑾峰彇鏁版嵁鏉冮檺鍒楄〃
+ *
+ * @param bizType 鏁版嵁绫诲瀷
+ * @param bizId 鏁版嵁缂栧彿
+ * @return 鏉冮檺鍒楄〃
+ */
+ List<AfterSalePermissionDO> getPermissionListByBiz(Integer bizType, Long bizId);
+
+ /**
+ * 鑾峰彇鏁版嵁鏉冮檺鍒楄〃锛堟壒閲忥級
+ *
+ * @param bizType 鏁版嵁绫诲瀷
+ * @param bizIds 鏁版嵁缂栧彿鍒楄〃
+ * @return 鏉冮檺鍒楄〃
+ */
+ List<AfterSalePermissionDO> getPermissionListByBiz(Integer bizType, Collection<Long> bizIds);
+
+ /**
+ * 鏍¢獙鏄惁鏈夋寚瀹氭暟鎹殑鎿嶄綔鏉冮檺
+ *
+ * @param bizType 鏁版嵁绫诲瀷
+ * @param bizId 鏁版嵁缂栧彿
+ * @param userId 鐢ㄦ埛缂栧彿
+ * @param level 鏉冮檺绾у埆
+ * @return 鏄惁鏈夋潈闄�
+ */
+ boolean hasPermission(Integer bizType, Long bizId, Long userId, AfterSalePermissionLevelEnum level);
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/AfterSalePermissionServiceImpl.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/AfterSalePermissionServiceImpl.java
new file mode 100644
index 0000000..219369e
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/AfterSalePermissionServiceImpl.java
@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.module.aftersales.service.permission;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.permission.AfterSalePermissionDO;
+import cn.iocoder.yudao.module.aftersales.dal.mysql.permission.AfterSalePermissionMapper;
+import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
+import cn.iocoder.yudao.module.aftersales.service.permission.bo.AfterSalePermissionCreateReqBO;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 鍞悗妯″潡鏁版嵁鏉冮檺 Service 瀹炵幇绫�
+ */
+@Service
+@Validated
+public class AfterSalePermissionServiceImpl implements AfterSalePermissionService {
+
+ @Resource
+ private AfterSalePermissionMapper afterSalePermissionMapper;
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Long createPermission(AfterSalePermissionCreateReqBO createReqBO) {
+ AfterSalePermissionDO permission = BeanUtils.toBean(createReqBO, AfterSalePermissionDO.class);
+ afterSalePermissionMapper.insert(permission);
+ return permission.getId();
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void deletePermission(Integer bizType, Long bizId) {
+ afterSalePermissionMapper.deleteByBiz(bizType, bizId);
+ }
+
+ @Override
+ public List<AfterSalePermissionDO> getPermissionListByBiz(Integer bizType, Long bizId) {
+ return afterSalePermissionMapper.selectListByBiz(bizType, bizId);
+ }
+
+ @Override
+ public List<AfterSalePermissionDO> getPermissionListByBiz(Integer bizType, Collection<Long> bizIds) {
+ if (CollUtil.isEmpty(bizIds)) {
+ return Collections.emptyList();
+ }
+ return afterSalePermissionMapper.selectListByBiz(bizType, bizIds);
+ }
+
+ @Override
+ public boolean hasPermission(Integer bizType, Long bizId, Long userId, AfterSalePermissionLevelEnum level) {
+ List<AfterSalePermissionDO> permissionList = getPermissionListByBiz(bizType, bizId);
+ return permissionList.stream()
+ .anyMatch(p -> ObjUtil.equal(p.getUserId(), userId)
+ && ObjUtil.equal(p.getLevel(), level.getLevel()));
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/bo/AfterSalePermissionCreateReqBO.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/bo/AfterSalePermissionCreateReqBO.java
new file mode 100644
index 0000000..13bc328
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/permission/bo/AfterSalePermissionCreateReqBO.java
@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.aftersales.service.permission.bo;
+
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
+import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 鍞悗妯″潡鏁版嵁鏉冮檺鍒涘缓 Request BO
+ */
+@Data
+public class AfterSalePermissionCreateReqBO {
+
+ /**
+ * 鏁版嵁绫诲瀷锛屽叧鑱� {@link AfterSaleBizTypeEnum}
+ */
+ @NotNull(message = "鏁版嵁绫诲瀷涓嶈兘涓虹┖")
+ private Integer bizType;
+
+ /**
+ * 鏁版嵁缂栧彿
+ */
+ @NotNull(message = "鏁版嵁缂栧彿涓嶈兘涓虹┖")
+ private Long bizId;
+
+ /**
+ * 鐢ㄦ埛缂栧彿
+ */
+ @NotNull(message = "鐢ㄦ埛缂栧彿涓嶈兘涓虹┖")
+ private Long userId;
+
+ /**
+ * 鏉冮檺绾у埆锛屽叧鑱� {@link AfterSalePermissionLevelEnum}
+ */
+ @NotNull(message = "鏉冮檺绾у埆涓嶈兘涓虹┖")
+ private Integer level;
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairService.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairService.java
new file mode 100644
index 0000000..0c41603
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairService.java
@@ -0,0 +1,63 @@
+package cn.iocoder.yudao.module.aftersales.service.repair;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.repair.AfterSaleRepairDO;
+import jakarta.validation.Valid;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 鍞悗缁翠慨璁板綍 Service 鎺ュ彛
+ */
+public interface AfterSaleRepairService {
+
+ /**
+ * 鍒涘缓缁翠慨璁板綍
+ *
+ * @param repairDO 缁翠慨淇℃伅
+ * @return 缂栧彿
+ */
+ Long createRepair(@Valid AfterSaleRepairDO repairDO);
+
+ /**
+ * 鏇存柊缁翠慨璁板綍
+ *
+ * @param repairDO 鏇存柊淇℃伅
+ */
+ void updateRepair(@Valid AfterSaleRepairDO repairDO);
+
+ /**
+ * 瀹屾垚缁翠慨
+ *
+ * @param id 缁翠慨璁板綍缂栧彿
+ */
+ void completeRepair(Long id);
+
+ /**
+ * 鑾峰緱缁翠慨璁板綍
+ *
+ * @param id 缂栧彿
+ * @return 缁翠慨璁板綍
+ */
+ AfterSaleRepairDO getRepair(Long id);
+
+ /**
+ * 鑾峰緱缁翠慨璁板綍鍒楄〃
+ *
+ * @param ids 缂栧彿鍒楄〃
+ * @return 缁翠慨璁板綍鍒楄〃
+ */
+ List<AfterSaleRepairDO> getRepairList(Collection<Long> ids);
+
+ /**
+ * 鑾峰緱缁翠慨璁板綍鍒嗛〉
+ *
+ * @param pageParam 鍒嗛〉鍙傛暟
+ * @param ticketId 鍏宠仈宸ュ崟缂栧彿
+ * @param status 缁翠慨鐘舵��
+ * @return 缁翠慨璁板綍鍒嗛〉
+ */
+ PageResult<AfterSaleRepairDO> getRepairPage(Object pageParam, Long ticketId, Integer status);
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairServiceImpl.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairServiceImpl.java
new file mode 100644
index 0000000..41a2163
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/repair/AfterSaleRepairServiceImpl.java
@@ -0,0 +1,110 @@
+package cn.iocoder.yudao.module.aftersales.service.repair;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.repair.AfterSaleRepairDO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
+import cn.iocoder.yudao.module.aftersales.dal.mysql.repair.AfterSaleRepairMapper;
+import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketMapper;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleRepairStatusEnum;
+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.time.LocalDateTime;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.*;
+
+/**
+ * 鍞悗缁翠慨璁板綍 Service 瀹炵幇绫�
+ */
+@Service
+@Validated
+@Slf4j
+public class AfterSaleRepairServiceImpl implements AfterSaleRepairService {
+
+ @Resource
+ private AfterSaleRepairMapper repairMapper;
+ @Resource
+ private AfterSaleTicketMapper ticketMapper;
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Long createRepair(AfterSaleRepairDO repairDO) {
+ // 鏍¢獙宸ュ崟瀛樺湪
+ AfterSaleTicketDO ticket = ticketMapper.selectById(repairDO.getTicketId());
+ if (ticket == null) {
+ throw exception(TICKET_NOT_EXISTS);
+ }
+
+ repairDO.setStatus(AfterSaleRepairStatusEnum.PENDING.getStatus());
+ repairMapper.insert(repairDO);
+
+ // 鏇存柊宸ュ崟鍏宠仈缁翠慨璁板綍
+ ticketMapper.updateById(new AfterSaleTicketDO().setId(ticket.getId())
+ .setRepairId(repairDO.getId()));
+
+ return repairDO.getId();
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void updateRepair(AfterSaleRepairDO repairDO) {
+ validateRepairExists(repairDO.getId());
+ repairMapper.updateById(repairDO);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void completeRepair(Long id) {
+ AfterSaleRepairDO repair = validateRepairExists(id);
+ repairMapper.updateById(new AfterSaleRepairDO().setId(id)
+ .setStatus(AfterSaleRepairStatusEnum.COMPLETED.getStatus())
+ .setFinishTime(LocalDateTime.now()));
+ // 鏇存柊宸ュ崟鐘舵��
+ AfterSaleTicketDO ticket = ticketMapper.selectById(repair.getTicketId());
+ if (ticket != null) {
+ ticketMapper.updateById(new AfterSaleTicketDO().setId(ticket.getId())
+ .setStatus(cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketStatusEnum.COMPLETED.getStatus())
+ .setResolvedTime(LocalDateTime.now()));
+ }
+ }
+
+ @Override
+ public AfterSaleRepairDO getRepair(Long id) {
+ return validateRepairExists(id);
+ }
+
+ @Override
+ public List<AfterSaleRepairDO> getRepairList(Collection<Long> ids) {
+ if (CollUtil.isEmpty(ids)) {
+ return List.of();
+ }
+ return repairMapper.selectBatchIds(ids);
+ }
+
+ @Override
+ public PageResult<AfterSaleRepairDO> getRepairPage(Object pageParam, Long ticketId, Integer status) {
+ if (pageParam instanceof PageParam) {
+ return repairMapper.selectPage((PageParam) pageParam, ticketId, status);
+ }
+ return repairMapper.selectPage(new PageParam(), ticketId, status);
+ }
+
+ // ========== 鍐呴儴鏂规硶 ==========
+
+ private AfterSaleRepairDO validateRepairExists(Long id) {
+ AfterSaleRepairDO repair = repairMapper.selectById(id);
+ if (repair == null) {
+ throw exception(REPAIR_NOT_EXISTS);
+ }
+ return repair;
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnService.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnService.java
new file mode 100644
index 0000000..f3391f1
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnService.java
@@ -0,0 +1,92 @@
+package cn.iocoder.yudao.module.aftersales.service.returnobj;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo.AfterSaleReturnPageReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo.AfterSaleReturnSaveReqVO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnDO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnItemDO;
+import jakarta.validation.Valid;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 鍞悗閫�璐х敵璇� Service 鎺ュ彛
+ */
+public interface AfterSaleReturnService {
+
+ /**
+ * 鍒涘缓閫�璐х敵璇�
+ *
+ * @param createReqVO 鍒涘缓淇℃伅
+ * @param userId 鐢ㄦ埛缂栧彿
+ * @return 缂栧彿
+ */
+ Long createReturn(@Valid AfterSaleReturnSaveReqVO createReqVO, Long userId);
+
+ /**
+ * 瀹℃牳閫氳繃锛堣Е鍙� MES 閫�璐ф祦绋嬶級
+ *
+ * @param id 閫�璐х敵璇风紪鍙�
+ */
+ void approveReturn(Long id);
+
+ /**
+ * 鑾峰緱閫�璐х敵璇�
+ *
+ * @param id 缂栧彿
+ * @return 閫�璐х敵璇�
+ */
+ AfterSaleReturnDO getReturn(Long id);
+
+ /**
+ * 鑾峰緱閫�璐х敵璇峰垪琛�
+ *
+ * @param ids 缂栧彿鍒楄〃
+ * @return 閫�璐х敵璇峰垪琛�
+ */
+ List<AfterSaleReturnDO> getReturnList(Collection<Long> ids);
+
+ /**
+ * 鑾峰緱閫�璐х敵璇峰垎椤�
+ *
+ * @param pageReqVO 鍒嗛〉鏌ヨ
+ * @param userId 鐢ㄦ埛缂栧彿
+ * @return 閫�璐х敵璇峰垎椤�
+ */
+ PageResult<AfterSaleReturnDO> getReturnPage(AfterSaleReturnPageReqVO pageReqVO, Long userId);
+
+ /**
+ * 鑾峰緱閫�璐ф槑缁嗚鍒楄〃
+ *
+ * @param returnId 閫�璐х敵璇风紪鍙�
+ * @return 鏄庣粏琛屽垪琛�
+ */
+ List<AfterSaleReturnItemDO> getReturnItemListByReturnId(Long returnId);
+
+ /**
+ * MES 璐ㄦ瀹屾垚鍥炶皟
+ *
+ * @param returnId 閫�璐х敵璇风紪鍙�
+ * @param qualityResult 璐ㄦ缁撹
+ * @param reportUrl 璐ㄦ鎶ュ憡 URL
+ */
+ void onQualityFinished(Long returnId, String qualityResult, String reportUrl);
+
+ /**
+ * MES 閫�璐у叆搴撳畬鎴愬洖璋�
+ *
+ * @param returnId 閫�璐х敵璇风紪鍙�
+ */
+ void onInboundFinished(Long returnId);
+
+ /**
+ * ERP 閫�娆惧畬鎴愬洖璋�
+ *
+ * @param returnId 閫�璐х敵璇风紪鍙�
+ * @param actualRefundPrice 瀹為��閲戦
+ */
+ void onRefundCompleted(Long returnId, BigDecimal actualRefundPrice);
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java
new file mode 100644
index 0000000..ed5fcd6
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java
@@ -0,0 +1,221 @@
+package cn.iocoder.yudao.module.aftersales.service.returnobj;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo.AfterSaleReturnPageReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.returnobj.vo.AfterSaleReturnSaveReqVO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnDO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.returnobj.AfterSaleReturnItemDO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
+import cn.iocoder.yudao.module.aftersales.dal.mysql.returnobj.AfterSaleReturnItemMapper;
+import cn.iocoder.yudao.module.aftersales.dal.mysql.returnobj.AfterSaleReturnMapper;
+import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketMapper;
+import cn.iocoder.yudao.module.aftersales.dal.redis.no.AfterSaleNoRedisDAO;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleReturnStatusEnum;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketStatusEnum;
+import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
+import cn.iocoder.yudao.module.aftersales.service.permission.AfterSalePermissionService;
+import cn.iocoder.yudao.module.aftersales.service.permission.bo.AfterSalePermissionCreateReqBO;
+import cn.iocoder.yudao.module.mes.api.returnsales.MesWmReturnSalesApi;
+import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesCreateReqDTO;
+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.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.*;
+
+/**
+ * 鍞悗閫�璐х敵璇� Service 瀹炵幇绫�
+ */
+@Service
+@Validated
+@Slf4j
+public class AfterSaleReturnServiceImpl implements AfterSaleReturnService {
+
+ @Resource
+ private AfterSaleReturnMapper returnMapper;
+ @Resource
+ private AfterSaleReturnItemMapper returnItemMapper;
+ @Resource
+ private AfterSaleTicketMapper ticketMapper;
+
+ @Resource
+ private AfterSaleNoRedisDAO noRedisDAO;
+
+ @Resource
+ private AfterSalePermissionService permissionService;
+
+ @Resource
+ private MesWmReturnSalesApi mesWmReturnSalesApi;
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Long createReturn(AfterSaleReturnSaveReqVO createReqVO, Long userId) {
+ // 1. 鏍¢獙宸ュ崟瀛樺湪
+ AfterSaleTicketDO ticket = ticketMapper.selectById(createReqVO.getTicketId());
+ if (ticket == null) {
+ throw exception(TICKET_NOT_EXISTS);
+ }
+
+ // 2. 鐢熸垚鍗曞彿
+ String no = noRedisDAO.generate(AfterSaleNoRedisDAO.RETURN_NO_PREFIX);
+ if (returnMapper.selectByNo(no) != null) {
+ throw exception(RETURN_NO_EXISTS);
+ }
+
+ // 3. 璁$畻搴旈��鎬婚噾棰�
+ BigDecimal totalReturnPrice = BigDecimal.ZERO;
+ if (CollUtil.isNotEmpty(createReqVO.getItems())) {
+ totalReturnPrice = createReqVO.getItems().stream()
+ .map(item -> item.getReturnPrice() != null ? item.getReturnPrice() : BigDecimal.ZERO)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ }
+
+ // 4. 鎻掑叆閫�璐х敵璇�
+ AfterSaleReturnDO returnDO = BeanUtils.toBean(createReqVO, AfterSaleReturnDO.class)
+ .setNo(no)
+ .setTotalReturnPrice(totalReturnPrice)
+ .setStatus(AfterSaleReturnStatusEnum.DRAFT.getStatus());
+ returnMapper.insert(returnDO);
+
+ // 5. 鎻掑叆閫�璐ф槑缁嗚
+ if (CollUtil.isNotEmpty(createReqVO.getItems())) {
+ createReqVO.getItems().forEach(item -> {
+ AfterSaleReturnItemDO itemDO = BeanUtils.toBean(item, AfterSaleReturnItemDO.class);
+ itemDO.setReturnId(returnDO.getId());
+ returnItemMapper.insert(itemDO);
+ });
+ }
+
+ // 6. 鍒涘缓鏁版嵁鏉冮檺
+ AfterSalePermissionCreateReqBO permBO = new AfterSalePermissionCreateReqBO();
+ permBO.setBizType(AfterSaleBizTypeEnum.AFTER_SALE_RETURN.getType());
+ permBO.setBizId(returnDO.getId());
+ permBO.setUserId(userId);
+ permBO.setLevel(AfterSalePermissionLevelEnum.OWNER.getLevel());
+ permissionService.createPermission(permBO);
+
+ // 7. 鏇存柊宸ュ崟鐘舵�佷负寰呴��璐�
+ ticketMapper.updateById(new AfterSaleTicketDO().setId(ticket.getId())
+ .setReturnId(returnDO.getId())
+ .setStatus(AfterSaleTicketStatusEnum.WAITING_RETURN.getStatus()));
+
+ return returnDO.getId();
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void approveReturn(Long id) {
+ // 1. 鏍¢獙閫�璐х敵璇�
+ AfterSaleReturnDO returnDO = validateReturnExists(id);
+ if (!AfterSaleReturnStatusEnum.DRAFT.getStatus().equals(returnDO.getStatus())) {
+ throw exception(RETURN_APPROVE_FAIL);
+ }
+
+ // 2. 鑾峰彇鍏宠仈宸ュ崟锛堢敤浜庤幏鍙栭攢鍞鍗曞彿绛夊啑浣欎俊鎭級
+ AfterSaleTicketDO ticket = ticketMapper.selectById(returnDO.getTicketId());
+
+ // 3. 璋冪敤 MES 鍒涘缓閿�鍞��璐у崟
+ MesWmReturnSalesCreateReqDTO mesReqDTO = new MesWmReturnSalesCreateReqDTO();
+ mesReqDTO.setCode(returnDO.getNo());
+ mesReqDTO.setName("鍞悗閫�璐�-" + returnDO.getNo());
+ mesReqDTO.setSaleOrderId(returnDO.getSaleOrderId());
+ mesReqDTO.setSalesOrderCode(ticket != null ? ticket.getSaleOrderNo() : null);
+ mesReqDTO.setClientId(returnDO.getCustomerId());
+ mesReqDTO.setReturnDate(LocalDate.now());
+ mesReqDTO.setReturnReason(returnDO.getReturnReason());
+ mesReqDTO.setRemark("鍞悗宸ュ崟閫�璐� - 宸ュ崟鍙�: " + (ticket != null ? ticket.getNo() : ""));
+ mesReqDTO.setAfterSaleReturnId(returnDO.getId());
+ Long mesReturnId = mesWmReturnSalesApi.createReturnSales(mesReqDTO);
+
+ // 4. 鏇存柊閫�璐х敵璇风姸鎬�
+ returnMapper.updateById(new AfterSaleReturnDO().setId(id)
+ .setStatus(AfterSaleReturnStatusEnum.APPROVED.getStatus())
+ .setMesReturnId(mesReturnId)
+ .setMesReturnCode(returnDO.getNo())
+ .setAuditTime(LocalDateTime.now()));
+ }
+
+ @Override
+ public AfterSaleReturnDO getReturn(Long id) {
+ return validateReturnExists(id);
+ }
+
+ @Override
+ public List<AfterSaleReturnDO> getReturnList(Collection<Long> ids) {
+ if (CollUtil.isEmpty(ids)) {
+ return List.of();
+ }
+ return returnMapper.selectBatchIds(ids);
+ }
+
+ @Override
+ public PageResult<AfterSaleReturnDO> getReturnPage(AfterSaleReturnPageReqVO pageReqVO, Long userId) {
+ return returnMapper.selectPage(pageReqVO, userId, pageReqVO.getSceneType(),
+ pageReqVO.getNo(), pageReqVO.getTicketId(),
+ pageReqVO.getCustomerId(), pageReqVO.getStatus());
+ }
+
+ @Override
+ public List<AfterSaleReturnItemDO> getReturnItemListByReturnId(Long returnId) {
+ return returnItemMapper.selectListByReturnId(returnId);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void onQualityFinished(Long returnId, String qualityResult, String reportUrl) {
+ AfterSaleReturnDO returnDO = validateReturnExists(returnId);
+ returnMapper.updateById(new AfterSaleReturnDO().setId(returnId)
+ .setQualityResult(qualityResult)
+ .setQualityReportUrl(reportUrl)
+ .setStatus(AfterSaleReturnStatusEnum.INSPECTED.getStatus()));
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void onInboundFinished(Long returnId) {
+ AfterSaleReturnDO returnDO = validateReturnExists(returnId);
+ returnMapper.updateById(new AfterSaleReturnDO().setId(returnId)
+ .setStatus(AfterSaleReturnStatusEnum.INBOUND.getStatus())
+ .setReturnInboundTime(LocalDateTime.now()));
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void onRefundCompleted(Long returnId, BigDecimal actualRefundPrice) {
+ AfterSaleReturnDO returnDO = validateReturnExists(returnId);
+ returnMapper.updateById(new AfterSaleReturnDO().setId(returnId)
+ .setActualRefundPrice(actualRefundPrice)
+ .setStatus(AfterSaleReturnStatusEnum.REFUNDED.getStatus())
+ .setRefundTime(LocalDateTime.now()));
+
+ // 鏇存柊鍏宠仈宸ュ崟鐘舵�佷负寰呴��娆�/鎹㈣揣瀹屾垚
+ AfterSaleTicketDO ticket = ticketMapper.selectById(returnDO.getTicketId());
+ if (ticket != null) {
+ ticketMapper.updateById(new AfterSaleTicketDO().setId(ticket.getId())
+ .setStatus(AfterSaleTicketStatusEnum.WAITING_REFUND.getStatus()));
+ }
+ }
+
+ // ========== 鍐呴儴鏂规硶 ==========
+
+ private AfterSaleReturnDO validateReturnExists(Long id) {
+ AfterSaleReturnDO returnDO = returnMapper.selectById(id);
+ if (returnDO == null) {
+ throw exception(RETURN_NOT_EXISTS);
+ }
+ return returnDO;
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketService.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketService.java
new file mode 100644
index 0000000..7c3a9d0
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketService.java
@@ -0,0 +1,113 @@
+package cn.iocoder.yudao.module.aftersales.service.ticket;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketPageReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketSaveReqVO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketItemDO;
+import jakarta.validation.Valid;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 鍞悗宸ュ崟 Service 鎺ュ彛
+ */
+public interface AfterSaleTicketService {
+
+ /**
+ * 鍒涘缓宸ュ崟
+ *
+ * @param createReqVO 鍒涘缓淇℃伅
+ * @param userId 鐢ㄦ埛缂栧彿
+ * @return 缂栧彿
+ */
+ Long createTicket(@Valid AfterSaleTicketSaveReqVO createReqVO, Long userId);
+
+ /**
+ * 鏇存柊宸ュ崟
+ *
+ * @param updateReqVO 鏇存柊淇℃伅
+ */
+ void updateTicket(@Valid AfterSaleTicketSaveReqVO updateReqVO);
+
+ /**
+ * 鍒犻櫎宸ュ崟
+ *
+ * @param id 缂栧彿
+ */
+ void deleteTicket(Long id);
+
+ /**
+ * 鑾峰緱宸ュ崟
+ *
+ * @param id 缂栧彿
+ * @return 宸ュ崟
+ */
+ AfterSaleTicketDO getTicket(Long id);
+
+ /**
+ * 鑾峰緱宸ュ崟鍒楄〃
+ *
+ * @param ids 缂栧彿鍒楄〃
+ * @return 宸ュ崟鍒楄〃
+ */
+ List<AfterSaleTicketDO> getTicketList(Collection<Long> ids);
+
+ /**
+ * 鑾峰緱宸ュ崟鍒嗛〉
+ *
+ * @param pageReqVO 鍒嗛〉鏌ヨ
+ * @param userId 鐢ㄦ埛缂栧彿
+ * @return 宸ュ崟鍒嗛〉
+ */
+ PageResult<AfterSaleTicketDO> getTicketPage(AfterSaleTicketPageReqVO pageReqVO, Long userId);
+
+ /**
+ * 鑾峰緱宸ュ崟鏄庣粏琛屽垪琛�
+ *
+ * @param ticketId 宸ュ崟缂栧彿
+ * @return 鏄庣粏琛屽垪琛�
+ */
+ List<AfterSaleTicketItemDO> getTicketItemListByTicketId(Long ticketId);
+
+ /**
+ * 鍒嗛厤澶勭悊浜�
+ *
+ * @param id 宸ュ崟缂栧彿
+ * @param handlerUserId 澶勭悊浜虹敤鎴风紪鍙�
+ */
+ void assignTicket(Long id, Long handlerUserId);
+
+ /**
+ * 闂鍒ゅ畾锛堢‘瀹氶棶棰樼被鍨嬨�佸垎绫汇�佷弗閲嶇▼搴︼級
+ *
+ * @param id 宸ュ崟缂栧彿
+ * @param issueType 闂绫诲瀷
+ * @param issueCategory 闂鍒嗙被
+ * @param severityLevel 涓ラ噸绋嬪害
+ */
+ void classifyTicket(Long id, Integer issueType, String issueCategory, Integer severityLevel);
+
+ /**
+ * 瀹岀粨宸ュ崟
+ *
+ * @param id 宸ュ崟缂栧彿
+ */
+ void resolveTicket(Long id);
+
+ /**
+ * 鍏抽棴宸ュ崟
+ *
+ * @param id 宸ュ崟缂栧彿
+ */
+ void closeTicket(Long id);
+
+ /**
+ * 鍙栨秷宸ュ崟
+ *
+ * @param id 宸ュ崟缂栧彿
+ */
+ void cancelTicket(Long id);
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java
new file mode 100644
index 0000000..a076b58
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java
@@ -0,0 +1,212 @@
+package cn.iocoder.yudao.module.aftersales.service.ticket;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketPageReqVO;
+import cn.iocoder.yudao.module.aftersales.controller.admin.ticket.vo.ticket.AfterSaleTicketSaveReqVO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketItemDO;
+import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketItemMapper;
+import cn.iocoder.yudao.module.aftersales.dal.mysql.ticket.AfterSaleTicketMapper;
+import cn.iocoder.yudao.module.aftersales.dal.redis.no.AfterSaleNoRedisDAO;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleTicketStatusEnum;
+import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
+import cn.iocoder.yudao.module.aftersales.service.permission.AfterSalePermissionService;
+import cn.iocoder.yudao.module.aftersales.service.permission.bo.AfterSalePermissionCreateReqBO;
+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.time.LocalDateTime;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.*;
+
+/**
+ * 鍞悗宸ュ崟 Service 瀹炵幇绫�
+ */
+@Service
+@Validated
+@Slf4j
+public class AfterSaleTicketServiceImpl implements AfterSaleTicketService {
+
+ @Resource
+ private AfterSaleTicketMapper ticketMapper;
+ @Resource
+ private AfterSaleTicketItemMapper ticketItemMapper;
+
+ @Resource
+ private AfterSaleNoRedisDAO noRedisDAO;
+
+ @Resource
+ private AfterSalePermissionService permissionService;
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Long createTicket(AfterSaleTicketSaveReqVO createReqVO, Long userId) {
+ // 1. 鐢熸垚鍗曞彿
+ String no = noRedisDAO.generate(AfterSaleNoRedisDAO.TICKET_NO_PREFIX);
+ if (ticketMapper.selectByNo(no) != null) {
+ throw exception(TICKET_NO_EXISTS);
+ }
+
+ // 2. 鎻掑叆宸ュ崟
+ AfterSaleTicketDO ticket = BeanUtils.toBean(createReqVO, AfterSaleTicketDO.class)
+ .setNo(no)
+ .setStatus(AfterSaleTicketStatusEnum.DRAFT.getStatus())
+ .setOwnerUserId(userId);
+ ticketMapper.insert(ticket);
+
+ // 3. 鎻掑叆鏄庣粏琛�
+ if (CollUtil.isNotEmpty(createReqVO.getItems())) {
+ createReqVO.getItems().forEach(item -> {
+ AfterSaleTicketItemDO itemDO = BeanUtils.toBean(item, AfterSaleTicketItemDO.class);
+ itemDO.setTicketId(ticket.getId());
+ ticketItemMapper.insert(itemDO);
+ });
+ }
+
+ // 4. 鍒涘缓鏁版嵁鏉冮檺锛堣礋璐d汉锛�
+ AfterSalePermissionCreateReqBO permBO = new AfterSalePermissionCreateReqBO();
+ permBO.setBizType(AfterSaleBizTypeEnum.AFTER_SALE_TICKET.getType());
+ permBO.setBizId(ticket.getId());
+ permBO.setUserId(userId);
+ permBO.setLevel(AfterSalePermissionLevelEnum.OWNER.getLevel());
+ permissionService.createPermission(permBO);
+
+ return ticket.getId();
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void updateTicket(AfterSaleTicketSaveReqVO updateReqVO) {
+ // 1. 鏍¢獙瀛樺湪
+ AfterSaleTicketDO ticket = validateTicketExists(updateReqVO.getId());
+ validateTicketStatus(ticket, AfterSaleTicketStatusEnum.DRAFT);
+
+ // 2. 鏇存柊宸ュ崟
+ AfterSaleTicketDO updateObj = BeanUtils.toBean(updateReqVO, AfterSaleTicketDO.class);
+ ticketMapper.updateById(updateObj);
+
+ // 3. 鏇存柊鏄庣粏琛岋紙鍏堝垹鍚庡锛�
+ ticketItemMapper.deleteByTicketId(ticket.getId());
+ if (CollUtil.isNotEmpty(updateReqVO.getItems())) {
+ updateReqVO.getItems().forEach(item -> {
+ AfterSaleTicketItemDO itemDO = BeanUtils.toBean(item, AfterSaleTicketItemDO.class);
+ itemDO.setTicketId(ticket.getId());
+ ticketItemMapper.insert(itemDO);
+ });
+ }
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void deleteTicket(Long id) {
+ AfterSaleTicketDO ticket = validateTicketExists(id);
+ validateTicketStatus(ticket, AfterSaleTicketStatusEnum.DRAFT, AfterSaleTicketStatusEnum.CANCELED);
+ // 杞垹闄�
+ ticketMapper.deleteById(id);
+ ticketItemMapper.deleteByTicketId(id);
+ permissionService.deletePermission(AfterSaleBizTypeEnum.AFTER_SALE_TICKET.getType(), id);
+ }
+
+ @Override
+ public AfterSaleTicketDO getTicket(Long id) {
+ return validateTicketExists(id);
+ }
+
+ @Override
+ public List<AfterSaleTicketDO> getTicketList(Collection<Long> ids) {
+ if (CollUtil.isEmpty(ids)) {
+ return List.of();
+ }
+ return ticketMapper.selectBatchIds(ids);
+ }
+
+ @Override
+ public PageResult<AfterSaleTicketDO> getTicketPage(AfterSaleTicketPageReqVO pageReqVO, Long userId) {
+ return ticketMapper.selectPage(pageReqVO, userId, pageReqVO.getSceneType(),
+ pageReqVO.getNo(), pageReqVO.getName(),
+ pageReqVO.getCustomerId(), pageReqVO.getSaleOrderId(),
+ pageReqVO.getStatus(), pageReqVO.getIssueType());
+ }
+
+ @Override
+ public List<AfterSaleTicketItemDO> getTicketItemListByTicketId(Long ticketId) {
+ return ticketItemMapper.selectListByTicketId(ticketId);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void assignTicket(Long id, Long handlerUserId) {
+ AfterSaleTicketDO ticket = validateTicketExists(id);
+ validateTicketStatus(ticket, AfterSaleTicketStatusEnum.DRAFT, AfterSaleTicketStatusEnum.PROCESSING);
+ ticketMapper.updateById(new AfterSaleTicketDO().setId(id).setHandlerUserId(handlerUserId)
+ .setStatus(AfterSaleTicketStatusEnum.PROCESSING.getStatus()));
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void classifyTicket(Long id, Integer issueType, String issueCategory, Integer severityLevel) {
+ AfterSaleTicketDO ticket = validateTicketExists(id);
+ validateTicketStatus(ticket, AfterSaleTicketStatusEnum.PROCESSING);
+ ticketMapper.updateById(new AfterSaleTicketDO().setId(id)
+ .setIssueType(issueType).setIssueCategory(issueCategory).setSeverityLevel(severityLevel));
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void resolveTicket(Long id) {
+ AfterSaleTicketDO ticket = validateTicketExists(id);
+ validateTicketStatus(ticket, AfterSaleTicketStatusEnum.PROCESSING,
+ AfterSaleTicketStatusEnum.WAITING_RETURN, AfterSaleTicketStatusEnum.WAITING_REFUND);
+ ticketMapper.updateById(new AfterSaleTicketDO().setId(id)
+ .setStatus(AfterSaleTicketStatusEnum.COMPLETED.getStatus())
+ .setResolvedTime(LocalDateTime.now()));
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void closeTicket(Long id) {
+ AfterSaleTicketDO ticket = validateTicketExists(id);
+ validateTicketStatus(ticket, AfterSaleTicketStatusEnum.COMPLETED);
+ ticketMapper.updateById(new AfterSaleTicketDO().setId(id)
+ .setStatus(AfterSaleTicketStatusEnum.CLOSED.getStatus())
+ .setClosedTime(LocalDateTime.now()));
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void cancelTicket(Long id) {
+ AfterSaleTicketDO ticket = validateTicketExists(id);
+ validateTicketStatus(ticket, AfterSaleTicketStatusEnum.DRAFT, AfterSaleTicketStatusEnum.PROCESSING);
+ ticketMapper.updateById(new AfterSaleTicketDO().setId(id)
+ .setStatus(AfterSaleTicketStatusEnum.CANCELED.getStatus()));
+ }
+
+ // ========== 鍐呴儴鏂规硶 ==========
+
+ private AfterSaleTicketDO validateTicketExists(Long id) {
+ AfterSaleTicketDO ticket = ticketMapper.selectById(id);
+ if (ticket == null) {
+ throw exception(TICKET_NOT_EXISTS);
+ }
+ return ticket;
+ }
+
+ private void validateTicketStatus(AfterSaleTicketDO ticket, AfterSaleTicketStatusEnum... allowedStatuses) {
+ for (AfterSaleTicketStatusEnum status : allowedStatuses) {
+ if (status.getStatus().equals(ticket.getStatus())) {
+ return;
+ }
+ }
+ throw exception(TICKET_STATUS_ERROR);
+ }
+
+}
diff --git a/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/util/AfterSalePermissionUtils.java b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/util/AfterSalePermissionUtils.java
new file mode 100644
index 0000000..b8da995
--- /dev/null
+++ b/yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/util/AfterSalePermissionUtils.java
@@ -0,0 +1,94 @@
+package cn.iocoder.yudao.module.aftersales.util;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
+import cn.iocoder.yudao.module.aftersales.dal.dataobject.permission.AfterSalePermissionDO;
+import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
+import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
+import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSaleSceneTypeEnum;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
+import com.github.yulichang.autoconfigure.MybatisPlusJoinProperties;
+import com.github.yulichang.wrapper.MPJLambdaWrapper;
+import org.springframework.util.Assert;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+/**
+ * 鍞悗妯″潡鏁版嵁鏉冮檺宸ュ叿绫�
+ */
+public class AfterSalePermissionUtils {
+
+ /**
+ * 鏍¢獙鐢ㄦ埛鏄惁鏄敭鍚庣鐞嗗憳
+ *
+ * @return 鏄�/鍚�
+ */
+ public static boolean isAftersaleAdmin() {
+ PermissionCommonApi permissionApi = SpringUtil.getBean(PermissionCommonApi.class);
+ return permissionApi.hasAnyRoles(getLoginUserId(), "aftersales_admin");
+ }
+
+ /**
+ * 鏍¢獙鐢ㄦ埛鏄惁鏈夋煡鐪嬪叏閮ㄥ敭鍚庢暟鎹殑鏉冮檺
+ */
+ private static boolean hasViewAllPermission() {
+ PermissionCommonApi permissionApi = SpringUtil.getBean(PermissionCommonApi.class);
+ return permissionApi.hasAnyPermissions(getLoginUserId(), "aftersales:view-all");
+ }
+
+ /**
+ * 鏋勯�犲敭鍚庢ā鍧楁暟鎹�愬垎椤点�戞煡璇㈡潯浠�
+ *
+ * @param query 杩炶〃鏌ヨ瀵硅薄
+ * @param bizType 鏁版嵁绫诲瀷 {@link AfterSaleBizTypeEnum}
+ * @param bizId 鏁版嵁缂栧彿
+ * @param userId 鐢ㄦ埛缂栧彿
+ * @param sceneType 鍦烘櫙绫诲瀷
+ */
+ public static <T extends MPJLambdaWrapper<?>, S> void appendPermissionCondition(T query, Integer bizType,
+ SFunction<S, ?> bizId,
+ Long userId, Integer sceneType) {
+ MybatisPlusJoinProperties mybatisPlusJoinProperties = SpringUtil.getBean(MybatisPlusJoinProperties.class);
+ final String ownerUserIdField = mybatisPlusJoinProperties.getTableAlias() + ".owner_user_id";
+ // 鍦烘櫙涓�锛氭垜璐熻矗鐨勬暟鎹�
+ if (AfterSaleSceneTypeEnum.isOwner(sceneType)) {
+ query.eq(ownerUserIdField, userId);
+ }
+ // 鍦烘櫙浜岋細鎴戝弬涓庣殑鏁版嵁锛堟垜鏈夎鎴栧啓鏉冮檺锛屽苟涓斾笉鏄礋璐d汉锛�
+ if (AfterSaleSceneTypeEnum.isInvolved(sceneType)) {
+ if (isAftersaleAdmin()) {
+ return;
+ }
+ query.innerJoin(AfterSalePermissionDO.class, on -> on.eq(AfterSalePermissionDO::getBizType, bizType)
+ .eq(AfterSalePermissionDO::getBizId, bizId)
+ .in(AfterSalePermissionDO::getLevel, AfterSalePermissionLevelEnum.READ.getLevel(),
+ AfterSalePermissionLevelEnum.WRITE.getLevel())
+ .eq(AfterSalePermissionDO::getUserId, userId));
+ query.ne(ownerUserIdField, userId);
+ }
+ // 鍦烘櫙涓夛細涓嬪睘璐熻矗鐨勬暟鎹紙涓嬪睘鏄礋璐d汉锛�
+ if (AfterSaleSceneTypeEnum.isSubordinate(sceneType)) {
+ AdminUserApi adminUserApi = SpringUtil.getBean(AdminUserApi.class);
+ List<AdminUserRespDTO> subordinateUsers = adminUserApi.getUserListBySubordinate(userId);
+ if (CollUtil.isEmpty(subordinateUsers)) {
+ query.eq(ownerUserIdField, -1);
+ } else {
+ query.in(ownerUserIdField, convertSet(subordinateUsers, AdminUserRespDTO::getId));
+ }
+ }
+ // 鍦烘櫙鍥涳細鍏ㄩ儴鏁版嵁锛堥渶瑕佹湁 view-all 鏉冮檺鎴栧敭鍚庣鐞嗗憳锛�
+ if (AfterSaleSceneTypeEnum.isAll(sceneType)) {
+ if (isAftersaleAdmin() || hasViewAllPermission()) {
+ return;
+ }
+ query.eq(ownerUserIdField, userId);
+ }
+ }
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestRespVO.java
index 0107c1f..3debc10 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestRespVO.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestRespVO.java
@@ -5,6 +5,7 @@
import lombok.Data;
import java.math.BigDecimal;
+import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@@ -119,7 +120,7 @@
private BigDecimal taxPrice;
@Schema(description = "闇�姹傛棩鏈�")
- private LocalDateTime demandTime;
+ private LocalDate demandTime;
@Schema(description = "澶囨敞", example = "澶囨敞")
private String remark;
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java
index 2019f86..d12da09 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java
@@ -7,6 +7,7 @@
import lombok.Data;
import java.math.BigDecimal;
+import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@@ -70,7 +71,7 @@
private BigDecimal taxPercent;
@Schema(description = "闇�姹傛棩鏈�")
- private LocalDateTime demandTime;
+ private LocalDate demandTime;
@Schema(description = "澶囨敞", example = "澶囨敞")
private String remark;
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java
index 7005b01..a96501b 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java
@@ -19,6 +19,7 @@
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi;
+import cn.iocoder.yudao.module.mes.api.productsales.MesWmProductSalesLineApi;
import cn.iocoder.yudao.module.mes.api.salesnotice.MesWmSalesNoticeApi;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -60,6 +61,8 @@
private AdminUserApi adminUserApi;
@Resource
private MesWmSalesNoticeApi salesNoticeApi;
+ @Resource
+ private MesWmProductSalesLineApi productSalesLineApi;
@Resource
private MesProMpsApi mpsApi;
@Resource
@@ -128,10 +131,13 @@
List<ErpSaleOrderItemDO> saleOrderItemList = saleOrderService.getSaleOrderItemListByOrderId(id);
Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
convertSet(saleOrderItemList, ErpSaleOrderItemDO::getProductId));
+ Map<Long, String> batchCodeMap = productSalesLineApi.getBatchCodesBySaleOrderItemIds(
+ convertSet(saleOrderItemList, ErpSaleOrderItemDO::getId));
return success(BeanUtils.toBean(saleOrder, ErpSaleOrderRespVO.class, saleOrderVO ->
saleOrderVO.setItems(BeanUtils.toBean(saleOrderItemList, ErpSaleOrderRespVO.Item.class, item -> {
// 搴撳瓨淇℃伅鏆備笉鏌ヨ锛堜粨鍌ㄧ墿娴佹ā鍧楋級
item.setStockCount(BigDecimal.ZERO);
+ item.setBatchCode(batchCodeMap.get(item.getId()));
MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem -> item.setProductName(mdmItem.getName())
.setProductBarCode(mdmItem.getBarCode()));
})).setAttachmentList(storageAttachmentApi.listAttachments("erp_sale_order", id))));
@@ -177,7 +183,10 @@
// 1.5 鍙戣揣閫氱煡鍗曚俊鎭�
Map<Long, Boolean> salesNoticeMap = salesNoticeApi.checkSalesNoticeBySaleOrderIds(
convertSet(pageResult.getList(), ErpSaleOrderDO::getId));
- // 1.6 妫�鏌ユ槸鍚﹀彲浠ョ敓鎴愬彂璐ч�氱煡鍗曪紙闇�瑕佺敓浜х殑鏄庣粏瀵瑰簲鐨� MPS 鏄惁宸插畬鎴愶級
+ // 1.6 鍑哄簱鎵瑰彿
+ Map<Long, String> batchCodeMap = productSalesLineApi.getBatchCodesBySaleOrderItemIds(
+ convertSet(saleOrderItemList, ErpSaleOrderItemDO::getId));
+ // 1.7 妫�鏌ユ槸鍚﹀彲浠ョ敓鎴愬彂璐ч�氱煡鍗曪紙闇�瑕佺敓浜х殑鏄庣粏瀵瑰簲鐨� MPS 鏄惁宸插畬鎴愶級
// 绛涢�夊嚭闇�瑕佺敓浜х殑鏄庣粏ID
List<Long> needProductionItemIds = saleOrderItemList.stream()
.filter(item -> item.getNeedProduction() != null && item.getNeedProduction() == 1)
@@ -214,8 +223,11 @@
// 2. 寮�濮嬫嫾鎺�
return BeanUtils.toBean(pageResult, ErpSaleOrderRespVO.class, saleOrder -> {
saleOrder.setItems(BeanUtils.toBean(saleOrderItemMap.get(saleOrder.getId()), ErpSaleOrderRespVO.Item.class,
- item -> MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem -> item.setProductName(mdmItem.getName())
- .setProductBarCode(mdmItem.getBarCode()))));
+ item -> {
+ item.setBatchCode(batchCodeMap.get(item.getId()));
+ MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem -> item.setProductName(mdmItem.getName())
+ .setProductBarCode(mdmItem.getBarCode()));
+ }));
saleOrder.setProductNames(CollUtil.join(saleOrder.getItems(), "锛�", ErpSaleOrderRespVO.Item::getProductName));
MapUtils.findAndThen(customerMap, saleOrder.getCustomerId(), supplier -> saleOrder.setCustomerName(supplier.getName()));
MapUtils.findAndThen(userMap, Long.parseLong(saleOrder.getCreator()), user -> saleOrder.setCreatorName(user.getNickname()));
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderPageReqVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderPageReqVO.java
index 8d1e313..7f6bfc1 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderPageReqVO.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderPageReqVO.java
@@ -33,6 +33,15 @@
@Schema(description = "閿�鍞姸鎬�", example = "2")
private Integer status;
+ @Schema(description = "鍑哄簱鐘舵��", example = "3")
+ private Integer outStatus;
+
+ @Schema(description = "鐢熶骇鐘舵��", example = "0")
+ private Integer productionStatus;
+
+ @Schema(description = "鏄惁闇�瑕佺敓浜�", example = "1")
+ private Integer needProduction;
+
@Schema(description = "鍒涘缓鑰�")
private String creator;
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java
index f28a6e2..4070879 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java
@@ -164,6 +164,11 @@
@Schema(description = "鍏宠仈鐨� MPS 缂栧彿", example = "1024")
private Long mpsId;
+ // ========== 鍑哄簱鎵瑰彿 ==========
+
+ @Schema(description = "鍑哄簱鎵瑰彿", example = "B2026001銆丅2026002")
+ private String batchCode;
+
// ========== 鍏宠仈瀛楁 ==========
@Schema(description = "浜у搧鍚嶇О", requiredMode = Schema.RequiredMode.REQUIRED, example = "宸у厠鍔�")
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java
index 40ac455..f69cae5 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java
@@ -7,7 +7,7 @@
import lombok.*;
import java.math.BigDecimal;
-import java.time.LocalDateTime;
+import java.time.LocalDate;
/**
* ERP 閲囪喘鐢宠鏄庣粏 DO
@@ -71,7 +71,7 @@
/**
* 闇�姹傛棩鏈�
*/
- private LocalDateTime demandTime;
+ private LocalDate demandTime;
/**
* 澶囨敞
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java
index 7382197..2e1dcb9 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java
@@ -27,12 +27,15 @@
.eqIfPresent(ErpSaleOrderDO::getCustomerId, reqVO.getCustomerId())
.betweenIfPresent(ErpSaleOrderDO::getOrderTime, reqVO.getOrderTime())
.eqIfPresent(ErpSaleOrderDO::getStatus, reqVO.getStatus())
+ .eqIfPresent(ErpSaleOrderDO::getOutStatus, reqVO.getOutStatus())
+ .eqIfPresent(ErpSaleOrderDO::getProductionStatus, reqVO.getProductionStatus())
.likeIfPresent(ErpSaleOrderDO::getRemark, reqVO.getRemark())
.eqIfPresent(ErpSaleOrderDO::getCreator, reqVO.getCreator())
.orderByDesc(ErpSaleOrderDO::getId);
- if (reqVO.getProductId() != null) {
+ if (reqVO.getProductId() != null || reqVO.getNeedProduction() != null) {
query.leftJoin(ErpSaleOrderItemDO.class, ErpSaleOrderItemDO::getOrderId, ErpSaleOrderDO::getId)
.eq(reqVO.getProductId() != null, ErpSaleOrderItemDO::getProductId, reqVO.getProductId())
+ .eq(reqVO.getNeedProduction() != null, ErpSaleOrderItemDO::getNeedProduction, reqVO.getNeedProduction())
.groupBy(ErpSaleOrderDO::getId); // 閬垮厤 1 瀵瑰鏌ヨ锛屼骇鐢熺浉鍚岀殑 1
}
return selectJoinPage(reqVO, ErpSaleOrderDO.class, query);
@@ -47,6 +50,8 @@
.eqIfPresent(ErpSaleOrderDO::getCustomerId, reqVO.getCustomerId())
.betweenIfPresent(ErpSaleOrderDO::getOrderTime, reqVO.getOrderTime())
.eqIfPresent(ErpSaleOrderDO::getStatus, reqVO.getStatus())
+ .eqIfPresent(ErpSaleOrderDO::getOutStatus, reqVO.getOutStatus())
+ .eqIfPresent(ErpSaleOrderDO::getProductionStatus, reqVO.getProductionStatus())
.likeIfPresent(ErpSaleOrderDO::getRemark, reqVO.getRemark())
.eqIfPresent(ErpSaleOrderDO::getCreator, reqVO.getCreator())
.orderByDesc(ErpSaleOrderDO::getId);
@@ -57,9 +62,10 @@
}
query.in(ErpSaleOrderDO::getCustomerId, permittedCustomerIds);
}
- if (reqVO.getProductId() != null) {
+ if (reqVO.getProductId() != null || reqVO.getNeedProduction() != null) {
query.leftJoin(ErpSaleOrderItemDO.class, ErpSaleOrderItemDO::getOrderId, ErpSaleOrderDO::getId)
.eq(reqVO.getProductId() != null, ErpSaleOrderItemDO::getProductId, reqVO.getProductId())
+ .eq(reqVO.getNeedProduction() != null, ErpSaleOrderItemDO::getNeedProduction, reqVO.getNeedProduction())
.groupBy(ErpSaleOrderDO::getId); // 閬垮厤 1 瀵瑰鏌ヨ锛屼骇鐢熺浉鍚岀殑 1
}
return selectJoinPage(reqVO, ErpSaleOrderDO.class, query);
diff --git a/yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/productsales/MesWmProductSalesLineApi.java b/yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/productsales/MesWmProductSalesLineApi.java
new file mode 100644
index 0000000..05bfeba
--- /dev/null
+++ b/yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/productsales/MesWmProductSalesLineApi.java
@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.mes.api.productsales;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * MES 閿�鍞嚭搴撳崟琛� API 鎺ュ彛
+ *
+ * @author 鑺嬮亾婧愮爜
+ */
+public interface MesWmProductSalesLineApi {
+
+ /**
+ * 鏍规嵁閿�鍞鍗曟槑缁� ID 鍒楄〃鏌ヨ鍑哄簱鎵瑰彿
+ *
+ * @param saleOrderItemIds 閿�鍞鍗曟槑缁� ID 鍒楄〃
+ * @return Map<閿�鍞鍗曟槑缁咺D, 鎵瑰彿涓茶仈瀛楃涓�>
+ */
+ Map<Long, String> getBatchCodesBySaleOrderItemIds(Collection<Long> saleOrderItemIds);
+
+}
diff --git a/yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApi.java b/yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApi.java
new file mode 100644
index 0000000..5953c83
--- /dev/null
+++ b/yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApi.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.mes.api.returnsales;
+
+import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesCreateReqDTO;
+
+/**
+ * MES 閿�鍞��璐у崟 API 鎺ュ彛
+ *
+ * @author 鑺嬮亾婧愮爜
+ */
+public interface MesWmReturnSalesApi {
+
+ /**
+ * 鍒涘缓閿�鍞��璐у崟锛堢敱鍞悗妯″潡璋冪敤锛�
+ *
+ * @param reqDTO 鍒涘缓璇锋眰鍙傛暟
+ * @return 鍒涘缓鐨勯攢鍞��璐у崟缂栧彿
+ */
+ Long createReturnSales(MesWmReturnSalesCreateReqDTO reqDTO);
+
+}
diff --git a/yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/dto/MesWmReturnSalesCreateReqDTO.java b/yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/dto/MesWmReturnSalesCreateReqDTO.java
new file mode 100644
index 0000000..8d4e7e5
--- /dev/null
+++ b/yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/dto/MesWmReturnSalesCreateReqDTO.java
@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.mes.api.returnsales.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDate;
+
+/**
+ * MES 閿�鍞��璐у崟鍒涘缓 Request DTO
+ *
+ * @author 鑺嬮亾婧愮爜
+ */
+@Schema(description = "RPC 鏈嶅姟 - MES 閿�鍞��璐у崟鍒涘缓 Request DTO")
+@Data
+public class MesWmReturnSalesCreateReqDTO {
+
+ @Schema(description = "閫�璐у崟缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "RS202603010001")
+ private String code;
+
+ @Schema(description = "閫�璐у崟鍚嶇О", requiredMode = Schema.RequiredMode.REQUIRED, example = "鍞悗閫�璐�")
+ private String name;
+
+ @Schema(description = "閿�鍞鍗� ID", example = "1")
+ private Long saleOrderId;
+
+ @Schema(description = "閿�鍞鍗曠紪鍙�", example = "SO202603010001")
+ private String salesOrderCode;
+
+ @Schema(description = "瀹㈡埛 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Long clientId;
+
+ @Schema(description = "閫�璐ф棩鏈�", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDate returnDate;
+
+ @Schema(description = "閫�璐у師鍥�", requiredMode = Schema.RequiredMode.REQUIRED, example = "璐ㄩ噺闂")
+ private String returnReason;
+
+ @Schema(description = "澶囨敞", example = "鍞悗閫�璐х敵璇峰叧鑱�")
+ private String remark;
+
+ @Schema(description = "鍞悗閫�璐х敵璇风紪鍙凤紙鍏宠仈 after_sale_return.id锛�", example = "1")
+ private Long afterSaleReturnId;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/productsales/MesWmProductSalesLineApiImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/productsales/MesWmProductSalesLineApiImpl.java
new file mode 100644
index 0000000..a59b512
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/productsales/MesWmProductSalesLineApiImpl.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.mes.api.productsales;
+
+import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesLineMapper;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * MES 閿�鍞嚭搴撳崟琛� API 瀹炵幇绫�
+ *
+ * @author 鑺嬮亾婧愮爜
+ */
+@Service
+@Validated
+public class MesWmProductSalesLineApiImpl implements MesWmProductSalesLineApi {
+
+ @Resource
+ private MesWmProductSalesLineMapper productSalesLineMapper;
+
+ @Override
+ public Map<Long, String> getBatchCodesBySaleOrderItemIds(Collection<Long> saleOrderItemIds) {
+ return productSalesLineMapper.selectBatchCodesBySaleOrderItemIds(saleOrderItemIds);
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApiImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApiImpl.java
new file mode 100644
index 0000000..67c278c
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApiImpl.java
@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.mes.api.returnsales;
+
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesCreateReqDTO;
+import cn.iocoder.yudao.module.mes.controller.admin.wm.returnsales.vo.MesWmReturnSalesSaveReqVO;
+import cn.iocoder.yudao.module.mes.service.wm.returnsales.MesWmReturnSalesService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+/**
+ * MES 閿�鍞��璐у崟 API 瀹炵幇绫�
+ *
+ * @author 鑺嬮亾婧愮爜
+ */
+@Service
+public class MesWmReturnSalesApiImpl implements MesWmReturnSalesApi {
+
+ @Resource
+ private MesWmReturnSalesService returnSalesService;
+
+ @Override
+ public Long createReturnSales(MesWmReturnSalesCreateReqDTO reqDTO) {
+ MesWmReturnSalesSaveReqVO createReqVO = BeanUtils.toBean(reqDTO, MesWmReturnSalesSaveReqVO.class);
+ return returnSalesService.createReturnSales(createReqVO);
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesPageReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesPageReqVO.java
index 17ef39a..3a2e459 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesPageReqVO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesPageReqVO.java
@@ -6,9 +6,9 @@
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
-import java.time.LocalDateTime;
+import java.time.LocalDate;
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
/**
* 绠$悊鍚庡彴 - MES 閿�鍞��璐х浉鍏�
@@ -26,6 +26,9 @@
@Schema(description = "閫�璐у崟鍚嶇О", example = "閿�鍞��璐�")
private String name;
+ @Schema(description = "閿�鍞鍗� ID", example = "1")
+ private Long saleOrderId;
+
@Schema(description = "閿�鍞鍗曠紪鍙�", example = "SO202603010001")
private String salesOrderCode;
@@ -36,7 +39,7 @@
private Integer status;
@Schema(description = "閫�璐ф棩鏈�")
- @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
- private LocalDateTime[] returnDate;
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
+ private LocalDate[] returnDate;
}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesRespVO.java
index 14b8f86..8b8f429 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesRespVO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesRespVO.java
@@ -3,6 +3,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
+import java.time.LocalDate;
import java.time.LocalDateTime;
/**
@@ -23,6 +24,9 @@
@Schema(description = "閫�璐у崟鍚嶇О", requiredMode = Schema.RequiredMode.REQUIRED, example = "閿�鍞��璐�")
private String name;
+ @Schema(description = "閿�鍞鍗� ID", example = "1")
+ private Long saleOrderId;
+
@Schema(description = "閿�鍞鍗曠紪鍙�", example = "SO202603010001")
private String salesOrderCode;
@@ -33,7 +37,7 @@
private String clientName;
@Schema(description = "閫�璐ф棩鏈�")
- private LocalDateTime returnDate;
+ private LocalDate returnDate;
@Schema(description = "閫�璐у師鍥�", example = "璐ㄩ噺闂")
private String returnReason;
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesSaveReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesSaveReqVO.java
index 15caf1f..f2c0654 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesSaveReqVO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesSaveReqVO.java
@@ -5,7 +5,7 @@
import jakarta.validation.constraints.NotNull;
import lombok.Data;
-import java.time.LocalDateTime;
+import java.time.LocalDate;
/**
* 绠$悊鍚庡彴 - MES 閿�鍞��璐у崟鏂板/淇敼 Request VO
@@ -27,6 +27,9 @@
@NotBlank(message = "閫�璐у崟鍚嶇О涓嶈兘涓虹┖")
private String name;
+ @Schema(description = "閿�鍞鍗� ID", example = "1")
+ private Long saleOrderId;
+
@Schema(description = "閿�鍞鍗曠紪鍙�", example = "SO202603010001")
private String salesOrderCode;
@@ -36,7 +39,7 @@
@Schema(description = "閫�璐ф棩鏈�", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "閫�璐ф棩鏈熶笉鑳戒负绌�")
- private LocalDateTime returnDate;
+ private LocalDate returnDate;
@Schema(description = "閫�璐у師鍥�", requiredMode = Schema.RequiredMode.REQUIRED, example = "璐ㄩ噺闂")
@NotBlank(message = "閫�璐у師鍥犱笉鑳戒负绌�")
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorRespVO.java
index e6f0263..f3936b0 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorRespVO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorRespVO.java
@@ -3,6 +3,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
+import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "绠$悊鍚庡彴 - MES 渚涘簲鍟嗛��璐у崟 Response VO")
@@ -34,7 +35,7 @@
private String vendorNickname;
@Schema(description = "閫�璐ф棩鏈�")
- private LocalDateTime returnDate;
+ private LocalDate returnDate;
@Schema(description = "閫�璐у師鍥�", example = "璐ㄩ噺涓嶅悎鏍�")
private String returnReason;
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorSaveReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorSaveReqVO.java
index 720348a..da089e2 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorSaveReqVO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorSaveReqVO.java
@@ -5,7 +5,7 @@
import jakarta.validation.constraints.NotNull;
import lombok.Data;
-import java.time.LocalDateTime;
+import java.time.LocalDate;
@Schema(description = "绠$悊鍚庡彴 - MES 渚涘簲鍟嗛��璐у崟鏂板/淇敼 Request VO")
@Data
@@ -31,7 +31,7 @@
@Schema(description = "閫�璐ф棩鏈�", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "閫�璐ф棩鏈熶笉鑳戒负绌�")
- private LocalDateTime returnDate;
+ private LocalDate returnDate;
@Schema(description = "閫�璐у師鍥�", example = "璐ㄩ噺涓嶅悎鏍�")
private String returnReason;
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnsales/MesWmReturnSalesDO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnsales/MesWmReturnSalesDO.java
index baa5daa..6faeffb 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnsales/MesWmReturnSalesDO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnsales/MesWmReturnSalesDO.java
@@ -7,7 +7,7 @@
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
-import java.time.LocalDateTime;
+import java.time.LocalDate;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
/**
@@ -39,6 +39,10 @@
*/
private String name;
/**
+ * 閿�鍞鍗� ID
+ */
+ private Long saleOrderId;
+ /**
* 閿�鍞鍗曠紪鍙�
*/
private String salesOrderCode;
@@ -51,7 +55,7 @@
/**
* 閫�璐ф棩鏈�
*/
- private LocalDateTime returnDate;
+ private LocalDate returnDate;
/**
* 閫�璐у師鍥�
*/
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnvendor/MesWmReturnVendorDO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnvendor/MesWmReturnVendorDO.java
index 9b8b4be..476fd5f 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnvendor/MesWmReturnVendorDO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnvendor/MesWmReturnVendorDO.java
@@ -7,7 +7,7 @@
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
-import java.time.LocalDateTime;
+import java.time.LocalDate;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
/**
@@ -49,7 +49,7 @@
/**
* 閫�璐ф棩鏈�
*/
- private LocalDateTime returnDate;
+ private LocalDate returnDate;
/**
* 閫�璐у師鍥�
*/
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/productsales/MesWmProductSalesLineMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/productsales/MesWmProductSalesLineMapper.java
index 9dcc9e4..6068a10 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/productsales/MesWmProductSalesLineMapper.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/productsales/MesWmProductSalesLineMapper.java
@@ -1,11 +1,13 @@
package cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesLineDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
+import java.util.Map;
/**
* MES 閿�鍞嚭搴撳崟琛� Mapper
@@ -31,6 +33,26 @@
.orderByDesc(MesWmProductSalesLineDO::getId));
}
+ /**
+ * 鏍规嵁閿�鍞鍗曟槑缁� ID 鍒楄〃鏌ヨ鍑哄簱琛岋紝鎸� saleOrderItemId 鍒嗙粍杩斿洖鎵瑰彿
+ */
+ default Map<Long, String> selectBatchCodesBySaleOrderItemIds(java.util.Collection<Long> saleOrderItemIds) {
+ if (saleOrderItemIds == null || saleOrderItemIds.isEmpty()) {
+ return java.util.Collections.emptyMap();
+ }
+ List<MesWmProductSalesLineDO> lines = selectList(new LambdaQueryWrapperX<MesWmProductSalesLineDO>()
+ .in(MesWmProductSalesLineDO::getSaleOrderItemId, saleOrderItemIds));
+ return lines.stream()
+ .filter(l -> l.getBatchCode() != null && !l.getBatchCode().isEmpty())
+ .collect(java.util.stream.Collectors.groupingBy(
+ MesWmProductSalesLineDO::getSaleOrderItemId,
+ java.util.LinkedHashMap::new,
+ java.util.stream.Collectors.collectingAndThen(
+ java.util.stream.Collectors.mapping(MesWmProductSalesLineDO::getBatchCode,
+ java.util.stream.Collectors.toSet()),
+ codes -> String.join("銆�", codes))));
+ }
+
default void updateQualityStatusByIds(List<Long> ids, Integer qualityStatus) {
update(new MesWmProductSalesLineDO().setQualityStatus(qualityStatus),
new LambdaUpdateWrapper<MesWmProductSalesLineDO>().in(MesWmProductSalesLineDO::getId, ids));
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java
index 1a7e647..63a5981 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java
@@ -53,6 +53,10 @@
HRM_RESIGNATION_APPLICATION("hrm_resignation_application", "HRM 绂昏亴鐢宠"),
HRM_ATTENDANCE_EXCEPTION("hrm_attendance_exception", "HRM 鑰冨嫟寮傚父"),
+ // ===== 鍞悗 =====
+ AFTER_SALE_TICKET("aftersales_ticket", "鍞悗宸ュ崟"),
+ AFTER_SALE_RETURN("aftersales_return", "鍞悗閫�璐х敵璇�"),
+
// ===== MES =====
MES_PD_DOCUMENT("mes_pd_document", "MES 璁捐璧勬枡");
diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml
index 6df01b4..d275fc8 100644
--- a/yudao-server/pom.xml
+++ b/yudao-server/pom.xml
@@ -150,6 +150,13 @@
<version>${revision}</version>
</dependency>
+ <!-- 鍞悗宸ュ崟绠$悊妯″潡 -->
+ <dependency>
+ <groupId>cn.iocoder.boot</groupId>
+ <artifactId>yudao-module-aftersales</artifactId>
+ <version>${revision}</version>
+ </dependency>
+
<!-- spring boot 閰嶇疆鎵�闇�渚濊禆 -->
<dependency>
<groupId>org.springframework.boot</groupId>
--
Gitblit v1.9.3