Commit 1946f6a5e9dcc601eb077f8fa2a923fbdf839179

Authored by hexiaodong
1 parent 6c321426

feat(douyin): 退款中状态、发货单重提重建销售出库单;wtSp/NCC 相关调整

- 抖音同步:after_sale_info.refund_status=1 识别退款中;MapOrderStatus 20→本地 Status 4
- 订单合并、运单/发货、前端筛选与创建运单页支持退款中(4)
- 手动更新发货单时清除 SalesOrderId,使含 ERP 赠品的明细可重新生成销售出库单
- 附带 antis-ncc-admin wtSp、NCC Extend、launchSettings、前端 config 等既有改动

Made-with: Cursor
Antis.Erp.Plat/antis-ncc-admin/src/views/wtSp/Form.vue
@@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
10 </el-col> 10 </el-col>
11 <el-col :span="24"> 11 <el-col :span="24">
12 <el-form-item label="商品品类" prop="pl"> 12 <el-form-item label="商品品类" prop="pl">
13 - <el-select v-model="dataForm.pl" placeholder="请选择" clearable :style='{"width":"100%"}' > 13 + <el-select v-model="dataForm.pl" placeholder="请选择(可多选)" clearable multiple collapse-tags :style='{"width":"100%"}' >
14 <el-option v-for="(item, index) in plOptions" :key="index" :label="item.F_Plmc" :value="item.F_Id" ></el-option> 14 <el-option v-for="(item, index) in plOptions" :key="index" :label="item.F_Plmc" :value="item.F_Id" ></el-option>
15 </el-select> 15 </el-select>
16 </el-form-item> 16 </el-form-item>
@@ -161,7 +161,7 @@ @@ -161,7 +161,7 @@
161 dataForm: { 161 dataForm: {
162 id:'', 162 id:'',
163 spmc:undefined, 163 spmc:undefined,
164 - pl:undefined, 164 + pl:[],
165 pp:undefined, 165 pp:undefined,
166 spbm:undefined, 166 spbm:undefined,
167 dyspid:undefined, 167 dyspid:undefined,
@@ -313,6 +313,9 @@ @@ -313,6 +313,9 @@
313 this.dataForm = res.data; 313 this.dataForm = res.data;
314 if(!this.dataForm.spzt)this.dataForm.spzt=[]; 314 if(!this.dataForm.spzt)this.dataForm.spzt=[];
315 if(!this.dataForm.xsmd)this.dataForm.xsmd=[]; 315 if(!this.dataForm.xsmd)this.dataForm.xsmd=[];
  316 + // 商品品类:接口为逗号分隔的多个 F_Id
  317 + const plRaw = this.dataForm.pl;
  318 + this.dataForm.pl = plRaw ? String(plRaw).split(',').map(s => s.trim()).filter(Boolean) : [];
316 this.getStock(this.dataForm.spbm); 319 this.getStock(this.dataForm.spbm);
317 }) 320 })
318 } else { 321 } else {
@@ -386,7 +389,9 @@ @@ -386,7 +389,9 @@
386 tcfs_bl: this.dataForm.tcfs_bl || null, 389 tcfs_bl: this.dataForm.tcfs_bl || null,
387 spxlhType: this.dataForm.spxlhType || null, 390 spxlhType: this.dataForm.spxlhType || null,
388 xsqd: this.dataForm.xsqd || null, 391 xsqd: this.dataForm.xsqd || null,
389 - pl: this.dataForm.pl || null, 392 + pl: Array.isArray(this.dataForm.pl) && this.dataForm.pl.length
  393 + ? this.dataForm.pl.join(',')
  394 + : (this.dataForm.pl && typeof this.dataForm.pl === 'string' ? this.dataForm.pl : null),
390 pp: this.dataForm.pp || null, 395 pp: this.dataForm.pp || null,
391 spbm: this.dataForm.spbm || null, 396 spbm: this.dataForm.spbm || null,
392 dyspid: this.dataForm.dyspid || null, 397 dyspid: this.dataForm.dyspid || null,
Antis.Erp.Plat/antis-ncc-admin/src/views/wtSp/index.vue
1 -<template> 1 +<template>
2 <div class="NCC-common-layout"> 2 <div class="NCC-common-layout">
3 <div class="NCC-common-layout-center"> 3 <div class="NCC-common-layout-center">
4 <el-row class="NCC-common-search-box" :gutter="16"> 4 <el-row class="NCC-common-search-box" :gutter="16">
@@ -101,8 +101,8 @@ @@ -101,8 +101,8 @@
101 </div> 101 </div>
102 <NCC-table v-loading="listLoading" :data="list" has-c @selection-change="handleSelectionChange"> 102 <NCC-table v-loading="listLoading" :data="list" has-c @selection-change="handleSelectionChange">
103 <el-table-column prop="spmc" label="商品名称" align="left" /> 103 <el-table-column prop="spmc" label="商品名称" align="left" />
104 - <el-table-column label="商品品类" prop="pl" align="left">  
105 - <template slot-scope="scope">{{ scope.row.pl | dynamicText(plOptions) }}</template> 104 + <el-table-column label="商品品类" prop="pl" align="left" min-width="140" show-overflow-tooltip>
  105 + <template slot-scope="scope">{{ scope.row.pl }}</template>
106 </el-table-column> 106 </el-table-column>
107 <el-table-column label="商品品牌" prop="pp" align="left"> 107 <el-table-column label="商品品牌" prop="pp" align="left">
108 <template slot-scope="scope">{{ scope.row.pp | dynamicText(ppOptions) }}</template> 108 <template slot-scope="scope">{{ scope.row.pp | dynamicText(ppOptions) }}</template>
Antis.Erp.Plat/douyin/DouyinLogistics.API/Controllers/OrdersController.cs
@@ -5,6 +5,7 @@ using SqlSugar; @@ -5,6 +5,7 @@ using SqlSugar;
5 using Newtonsoft.Json; 5 using Newtonsoft.Json;
6 using Newtonsoft.Json.Linq; 6 using Newtonsoft.Json.Linq;
7 using System.Net.Http; 7 using System.Net.Http;
  8 +using System.Net.Sockets;
8 using System.Linq; 9 using System.Linq;
9 10
10 namespace DouyinLogistics.API.Controllers; 11 namespace DouyinLogistics.API.Controllers;
@@ -40,6 +41,8 @@ public class OrdersController : ControllerBase @@ -40,6 +41,8 @@ public class OrdersController : ControllerBase
40 [FromQuery] string? trackingNumber = null, 41 [FromQuery] string? trackingNumber = null,
41 [FromQuery] string? productName = null, 42 [FromQuery] string? productName = null,
42 [FromQuery] bool? hasWaybill = null, 43 [FromQuery] bool? hasWaybill = null,
  44 + /// <summary>为 true 时:待发货 + 已有运单号 + 尚未成功提交发货单(waybills 无 SalesOrderId)</summary>
  45 + [FromQuery] bool pendingShipmentForm = false,
43 [FromQuery] DateTime? createTimeStart = null, 46 [FromQuery] DateTime? createTimeStart = null,
44 [FromQuery] DateTime? createTimeEnd = null, 47 [FromQuery] DateTime? createTimeEnd = null,
45 [FromQuery] DateTime? payTimeStart = null, 48 [FromQuery] DateTime? payTimeStart = null,
@@ -52,7 +55,7 @@ public class OrdersController : ControllerBase @@ -52,7 +55,7 @@ public class OrdersController : ControllerBase
52 55
53 var (orders, total) = await _orderService.GetOrdersWithPagingAsync( 56 var (orders, total) = await _orderService.GetOrdersWithPagingAsync(
54 pageIndex, pageSize, status, orderId, receiverName, receiverPhone, 57 pageIndex, pageSize, status, orderId, receiverName, receiverPhone,
55 - trackingNumber, productName, hasWaybill, createTimeStart, createTimeEnd, payTimeStart, payTimeEnd); 58 + trackingNumber, productName, hasWaybill, pendingShipmentForm, createTimeStart, createTimeEnd, payTimeStart, payTimeEnd);
56 return Ok(new { data = orders, total, pageIndex, pageSize }); 59 return Ok(new { data = orders, total, pageIndex, pageSize });
57 } 60 }
58 catch (Exception ex) 61 catch (Exception ex)
@@ -476,6 +479,11 @@ public class OrdersController : ControllerBase @@ -476,6 +479,11 @@ public class OrdersController : ControllerBase
476 return BadRequest(new { message = "订单已存在运单号" }); 479 return BadRequest(new { message = "订单已存在运单号" });
477 } 480 }
478 481
  482 + if (order.Status == 2 || order.Status == 3 || order.Status == 4)
  483 + {
  484 + return BadRequest(new { message = "已取消、已退款或退款中的订单不允许创建运单" });
  485 + }
  486 +
479 // 调试日志 487 // 调试日志
480 Console.WriteLine($"🔍 [控制器] 创建运单请求: orderId={id}, 使用订单中的手机号: {order.ReceiverPhone}"); 488 Console.WriteLine($"🔍 [控制器] 创建运单请求: orderId={id}, 使用订单中的手机号: {order.ReceiverPhone}");
481 _logger.LogInformation($"创建运单请求: orderId={id}, 使用订单中的手机号: {order.ReceiverPhone}"); 489 _logger.LogInformation($"创建运单请求: orderId={id}, 使用订单中的手机号: {order.ReceiverPhone}");
@@ -619,7 +627,7 @@ public class OrdersController : ControllerBase @@ -619,7 +627,7 @@ public class OrdersController : ControllerBase
619 [FromQuery] string? spmc = null, // 商品名称 627 [FromQuery] string? spmc = null, // 商品名称
620 [FromQuery] string? spbm = null, // 商品编码 628 [FromQuery] string? spbm = null, // 商品编码
621 [FromQuery] string? dyspid = null, // 抖音SKU编码 629 [FromQuery] string? dyspid = null, // 抖音SKU编码
622 - [FromQuery] string? pl = null, // 商品品类(wt_pl.F_Id 630 + [FromQuery] string? pl = null, // 商品品类(单个 wt_pl.F_Id;商品可多品类逗号存储,ERP 按包含匹配
623 [FromQuery] int pageIndex = 1, 631 [FromQuery] int pageIndex = 1,
624 [FromQuery] int pageSize = 20) 632 [FromQuery] int pageSize = 20)
625 { 633 {
@@ -789,7 +797,8 @@ public class OrdersController : ControllerBase @@ -789,7 +797,8 @@ public class OrdersController : ControllerBase
789 catch (Exception ex) 797 catch (Exception ex)
790 { 798 {
791 _logger.LogError(ex, "查询ERP商品失败"); 799 _logger.LogError(ex, "查询ERP商品失败");
792 - return StatusCode(500, new { message = "查询ERP商品失败", error = ex.Message }); 800 + var hint = GetErpConnectionHint(ex);
  801 + return StatusCode(500, new { message = "查询ERP商品失败", error = hint ?? ex.Message });
793 } 802 }
794 } 803 }
795 804
@@ -861,7 +870,8 @@ public class OrdersController : ControllerBase @@ -861,7 +870,8 @@ public class OrdersController : ControllerBase
861 var data = result["data"]; 870 var data = result["data"];
862 if (data == null || data.Type == Newtonsoft.Json.Linq.JTokenType.Null) 871 if (data == null || data.Type == Newtonsoft.Json.Linq.JTokenType.Null)
863 { 872 {
864 - return NotFound(new { message = "未找到商品信息" }); 873 + // 使用 200 + data=null,避免前端 axios 将 HTTP 404 当作异常
  874 + return Ok(new { code = 200, data = (object?)null, message = "未找到商品信息" });
865 } 875 }
866 876
867 // 解析商品列表 877 // 解析商品列表
@@ -877,7 +887,7 @@ public class OrdersController : ControllerBase @@ -877,7 +887,7 @@ public class OrdersController : ControllerBase
877 887
878 if (list == null || (list is Newtonsoft.Json.Linq.JArray productArray && productArray.Count == 0)) 888 if (list == null || (list is Newtonsoft.Json.Linq.JArray productArray && productArray.Count == 0))
879 { 889 {
880 - return NotFound(new { message = "未找到商品信息" }); 890 + return Ok(new { code = 200, data = (object?)null, message = "未找到商品信息" });
881 } 891 }
882 892
883 // 获取第一个商品(或匹配的商品) 893 // 获取第一个商品(或匹配的商品)
@@ -892,6 +902,16 @@ public class OrdersController : ControllerBase @@ -892,6 +902,16 @@ public class OrdersController : ControllerBase
892 .FirstOrDefault(p => p["spbm"]?.ToString() == productCode); 902 .FirstOrDefault(p => p["spbm"]?.ToString() == productCode);
893 } 903 }
894 904
  905 + // 仅传 skuId 时:优先 dyspid 完全匹配(keyword 可能命中多条或误命中名称)
  906 + if (productObj == null && !string.IsNullOrEmpty(skuId))
  907 + {
  908 + productObj = productArray2
  909 + .OfType<Newtonsoft.Json.Linq.JObject>()
  910 + .FirstOrDefault(p =>
  911 + string.Equals(p["dyspid"]?.ToString(), skuId, StringComparison.Ordinal)
  912 + || string.Equals(p["Dyspid"]?.ToString(), skuId, StringComparison.Ordinal));
  913 + }
  914 +
895 // 如果没找到完全匹配的,使用第一个 915 // 如果没找到完全匹配的,使用第一个
896 if (productObj == null && productArray2.Count > 0) 916 if (productObj == null && productArray2.Count > 0)
897 { 917 {
@@ -901,6 +921,28 @@ public class OrdersController : ControllerBase @@ -901,6 +921,28 @@ public class OrdersController : ControllerBase
901 921
902 if (productObj != null) 922 if (productObj != null)
903 { 923 {
  924 + string? infoImageUrl = null;
  925 + var infoSpztRaw = productObj["spzt"]?.ToString();
  926 + if (!string.IsNullOrEmpty(infoSpztRaw))
  927 + {
  928 + try
  929 + {
  930 + var spztArr = JsonConvert.DeserializeObject<JArray>(infoSpztRaw);
  931 + if (spztArr != null && spztArr.Count > 0)
  932 + {
  933 + var firstImg = spztArr[0] as JObject;
  934 + var relUrl = firstImg?["url"]?.ToString();
  935 + if (!string.IsNullOrEmpty(relUrl))
  936 + {
  937 + infoImageUrl = relUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase)
  938 + ? relUrl
  939 + : erpApiConfig.BaseUrl.TrimEnd('/') + relUrl;
  940 + }
  941 + }
  942 + }
  943 + catch { /* ignore */ }
  944 + }
  945 +
904 return Ok(new 946 return Ok(new
905 { 947 {
906 code = 200, 948 code = 200,
@@ -909,17 +951,18 @@ public class OrdersController : ControllerBase @@ -909,17 +951,18 @@ public class OrdersController : ControllerBase
909 id = productObj["id"]?.ToString(), 951 id = productObj["id"]?.ToString(),
910 spbm = productObj["spbm"]?.ToString(), 952 spbm = productObj["spbm"]?.ToString(),
911 spmc = productObj["spmc"]?.ToString(), 953 spmc = productObj["spmc"]?.ToString(),
912 - spxlhType = productObj["spxlhType"]?.ToString(), // 序列号类型 954 + spxlhType = productObj["spxlhType"]?.ToString() ?? productObj["SpxlhType"]?.ToString(),
913 lsj = productObj["lsj"]?.ToObject<decimal>() ?? 0, 955 lsj = productObj["lsj"]?.ToObject<decimal>() ?? 0,
914 dw = productObj["dw"]?.ToString(), 956 dw = productObj["dw"]?.ToString(),
915 gg = productObj["gg"]?.ToString(), 957 gg = productObj["gg"]?.ToString(),
916 - dyspid = productObj["dyspid"]?.ToString() 958 + dyspid = productObj["dyspid"]?.ToString(),
  959 + imageUrl = infoImageUrl
917 } 960 }
918 }); 961 });
919 } 962 }
920 } 963 }
921 964
922 - return NotFound(new { message = "未找到商品信息" }); 965 + return Ok(new { code = 200, data = (object?)null, message = "未找到商品信息" });
923 } 966 }
924 catch (Exception ex) 967 catch (Exception ex)
925 { 968 {
@@ -1342,7 +1385,8 @@ public class OrdersController : ControllerBase @@ -1342,7 +1385,8 @@ public class OrdersController : ControllerBase
1342 catch (Exception ex) 1385 catch (Exception ex)
1343 { 1386 {
1344 _logger.LogError(ex, "查询仓库列表失败"); 1387 _logger.LogError(ex, "查询仓库列表失败");
1345 - return StatusCode(500, new { message = "查询仓库列表失败", error = ex.Message }); 1388 + var hint = GetErpConnectionHint(ex);
  1389 + return StatusCode(500, new { message = "查询仓库列表失败", error = hint ?? ex.Message });
1346 } 1390 }
1347 } 1391 }
1348 1392
@@ -1402,6 +1446,11 @@ public class OrdersController : ControllerBase @@ -1402,6 +1446,11 @@ public class OrdersController : ControllerBase
1402 return BadRequest(new { message = "订单尚未创建运单,请先创建运单" }); 1446 return BadRequest(new { message = "订单尚未创建运单,请先创建运单" });
1403 } 1447 }
1404 1448
  1449 + if (order.Status == 2 || order.Status == 3 || order.Status == 4)
  1450 + {
  1451 + return BadRequest(new { message = "已取消、已退款或退款中的订单不允许发货" });
  1452 + }
  1453 +
1405 var result = await _orderService.ShipToDouyinAsync(id); 1454 var result = await _orderService.ShipToDouyinAsync(id);
1406 if (result.Success) 1455 if (result.Success)
1407 { 1456 {
@@ -1510,6 +1559,18 @@ public class OrdersController : ControllerBase @@ -1510,6 +1559,18 @@ public class OrdersController : ControllerBase
1510 } 1559 }
1511 } 1560 }
1512 1561
  1562 + private static string? GetErpConnectionHint(Exception ex)
  1563 + {
  1564 + if (ex is HttpRequestException or SocketException or TaskCanceledException)
  1565 + return "无法连接 ERP:请确认 NCC.API 已启动,且 appsettings 中 ErpApi.BaseUrl 与 NCC 实际监听地址一致。";
  1566 + for (var e = ex.InnerException; e != null; e = e.InnerException)
  1567 + {
  1568 + if (e is HttpRequestException or SocketException)
  1569 + return "无法连接 ERP:请确认 NCC.API 已启动,且 appsettings 中 ErpApi.BaseUrl 与 NCC 实际监听地址一致。";
  1570 + }
  1571 + return null;
  1572 + }
  1573 +
1513 /// <summary> 1574 /// <summary>
1514 /// 清理所有订单并重新同步(天数由 appsettings.json 的 Douyin.SyncDays 配置,默认30天) 1575 /// 清理所有订单并重新同步(天数由 appsettings.json 的 Douyin.SyncDays 配置,默认30天)
1515 /// </summary> 1576 /// </summary>
Antis.Erp.Plat/douyin/DouyinLogistics.API/Controllers/WaybillController.cs
@@ -32,9 +32,9 @@ public class WaybillController : ControllerBase @@ -32,9 +32,9 @@ public class WaybillController : ControllerBase
32 { 32 {
33 return NotFound(new { message = "订单不存在" }); 33 return NotFound(new { message = "订单不存在" });
34 } 34 }
35 - if (order.Status == 2 || order.Status == 3) 35 + if (order.Status == 2 || order.Status == 3 || order.Status == 4)
36 { 36 {
37 - return BadRequest(new { message = "已取消或已退款的订单不允许进行任何操作" }); 37 + return BadRequest(new { message = "已取消、已退款或退款中的订单不允许进行任何操作" });
38 } 38 }
39 39
40 var preview = await _orderService.GetWaybillPreviewAsync(orderId); 40 var preview = await _orderService.GetWaybillPreviewAsync(orderId);
@@ -67,9 +67,9 @@ public class WaybillController : ControllerBase @@ -67,9 +67,9 @@ public class WaybillController : ControllerBase
67 { 67 {
68 return NotFound(new { message = "订单不存在" }); 68 return NotFound(new { message = "订单不存在" });
69 } 69 }
70 - if (order.Status == 2 || order.Status == 3) 70 + if (order.Status == 2 || order.Status == 3 || order.Status == 4)
71 { 71 {
72 - return BadRequest(new { message = "已取消或已退款的订单不允许进行任何操作" }); 72 + return BadRequest(new { message = "已取消、已退款或退款中的订单不允许进行任何操作" });
73 } 73 }
74 74
75 var result = await _orderService.PrintWaybillAsync(orderId); 75 var result = await _orderService.PrintWaybillAsync(orderId);
@@ -110,9 +110,9 @@ public class WaybillController : ControllerBase @@ -110,9 +110,9 @@ public class WaybillController : ControllerBase
110 { 110 {
111 return NotFound(new { message = "订单不存在" }); 111 return NotFound(new { message = "订单不存在" });
112 } 112 }
113 - if (order.Status == 2 || order.Status == 3) 113 + if (order.Status == 2 || order.Status == 3 || order.Status == 4)
114 { 114 {
115 - return BadRequest(new { message = "已取消或已退款的订单不允许进行任何操作" }); 115 + return BadRequest(new { message = "已取消、已退款或退款中的订单不允许进行任何操作" });
116 } 116 }
117 117
118 var result = await _orderService.PrintWaybillAndShipAsync(orderId); 118 var result = await _orderService.PrintWaybillAndShipAsync(orderId);
@@ -174,10 +174,10 @@ public class WaybillController : ControllerBase @@ -174,10 +174,10 @@ public class WaybillController : ControllerBase
174 return NotFound(new { message = "订单不存在" }); 174 return NotFound(new { message = "订单不存在" });
175 } 175 }
176 176
177 - // 检查订单状态,已取消/已退款的订单不允许创建发货单  
178 - if (order.Status == 2 || order.Status == 3) 177 + // 检查订单状态,已取消/已退款/退款中的订单不允许创建发货单
  178 + if (order.Status == 2 || order.Status == 3 || order.Status == 4)
179 { 179 {
180 - return BadRequest(new { message = "已取消或已退款的订单不允许进行任何操作" }); 180 + return BadRequest(new { message = "已取消、已退款或退款中的订单不允许进行任何操作" });
181 } 181 }
182 182
183 var db = HttpContext.RequestServices.GetRequiredService<ISqlSugarClient>(); 183 var db = HttpContext.RequestServices.GetRequiredService<ISqlSugarClient>();
@@ -261,6 +261,14 @@ public class WaybillController : ControllerBase @@ -261,6 +261,14 @@ public class WaybillController : ControllerBase
261 existingWaybill.Remark = request.Remark ?? existingWaybill.Remark; 261 existingWaybill.Remark = request.Remark ?? existingWaybill.Remark;
262 existingWaybill.UpdateTime = DateTime.Now; 262 existingWaybill.UpdateTime = DateTime.Now;
263 263
  264 + // 商品明细变更时清除已关联的销售出库单,让后续流程重新创建
  265 + if (!string.IsNullOrEmpty(existingWaybill.SalesOrderId))
  266 + {
  267 + _logger.LogInformation("发货单商品明细已更新,清除旧销售出库单关联: WaybillId={WaybillId}, OldSalesOrderId={OldSalesOrderId}",
  268 + existingWaybill.Id, existingWaybill.SalesOrderId);
  269 + existingWaybill.SalesOrderId = null;
  270 + }
  271 +
264 await db.Updateable(existingWaybill).ExecuteCommandAsync(); 272 await db.Updateable(existingWaybill).ExecuteCommandAsync();
265 waybill = existingWaybill; 273 waybill = existingWaybill;
266 waybillId = existingWaybill.Id; 274 waybillId = existingWaybill.Id;
Antis.Erp.Plat/douyin/DouyinLogistics.API/Models/Order.cs
@@ -18,7 +18,7 @@ public class Order @@ -18,7 +18,7 @@ public class Order
18 public string OrderId { get; set; } = string.Empty; 18 public string OrderId { get; set; } = string.Empty;
19 19
20 /// <summary> 20 /// <summary>
21 - /// 订单状态:0-待发货,1-已发货,2-已取消,3-已退款 21 + /// 订单状态:0-待发货,1-已发货,2-已取消,3-已退款,4-退款中
22 /// </summary> 22 /// </summary>
23 public int Status { get; set; } 23 public int Status { get; set; }
24 24
Antis.Erp.Plat/douyin/DouyinLogistics.API/Services/DouyinService.cs
1 using DouyinLogistics.API.Models; 1 using DouyinLogistics.API.Models;
2 using Newtonsoft.Json; 2 using Newtonsoft.Json;
  3 +using Newtonsoft.Json.Linq;
3 using System.Security.Cryptography; 4 using System.Security.Cryptography;
4 using System.Text; 5 using System.Text;
5 6
@@ -24,6 +25,107 @@ public class DouyinService @@ -24,6 +25,107 @@ public class DouyinService
24 } 25 }
25 26
26 /// <summary> 27 /// <summary>
  28 + /// 合并抖音主单/子单状态与文案,识别退款完结(21/22/39)与退款流程中。
  29 + /// 抖店在「退款中」时仍可能返回 order_status=2(待发货),仅靠 order_status 会误判。
  30 + /// 返回 20 表示退款处理中(内部约定,非抖音原始枚举),由 MapOrderStatus 映射为本地 4。
  31 + /// </summary>
  32 + private static int ResolveRefundAwareOrderStatus(JObject orderObj, int rawOrderStatus)
  33 + {
  34 + static bool IsRefundFinished(int s) => s is 21 or 22 or 39;
  35 +
  36 + var orderStatus = rawOrderStatus;
  37 + var mainStatus = orderObj["main_status"]?.ToObject<int?>();
  38 + var orderStatusDesc = orderObj["order_status_desc"]?.ToString() ?? "";
  39 + var mainStatusDesc = orderObj["main_status_desc"]?.ToString() ?? "";
  40 + var skuList = orderObj["sku_order_list"] as JArray;
  41 +
  42 + if (mainStatus.HasValue && IsRefundFinished(mainStatus.Value))
  43 + return mainStatus.Value;
  44 +
  45 + if (skuList != null)
  46 + {
  47 + foreach (var sku in skuList)
  48 + {
  49 + if (sku is not JObject so) continue;
  50 + var skuSt = so["order_status"]?.ToObject<int?>();
  51 + if (skuSt.HasValue && IsRefundFinished(skuSt.Value))
  52 + return skuSt.Value;
  53 + // 退款进行中时,主单/子单仍常为 order_status=2、文案「待发货」;真实状态在 after_sale_info(order.orderDetail 实测 refund_status=1)
  54 + var asi = so["after_sale_info"] as JObject;
  55 + if (asi != null)
  56 + {
  57 + var refundSt = asi["refund_status"]?.ToObject<int?>();
  58 + if (refundSt == 1)
  59 + return 20;
  60 + }
  61 + }
  62 + }
  63 +
  64 + static bool DescExcludesRefund(string a, string b)
  65 + {
  66 + var c = (a ?? "") + (b ?? "");
  67 + return c.Contains("不支持退款") || c.Contains("不可退款");
  68 + }
  69 +
  70 + static bool DescRefundInProgress(string a, string b)
  71 + {
  72 + if (DescExcludesRefund(a, b)) return false;
  73 + var c = (a ?? "") + (b ?? "");
  74 + return c.Contains("退款中") || c.Contains("退款申请") || c.Contains("售后中")
  75 + || c.Contains("售后处理") || c.Contains("仅退款") || c.Contains("退货退款");
  76 + }
  77 +
  78 + static bool DescRefundDone(string a, string b)
  79 + {
  80 + if (DescExcludesRefund(a, b)) return false;
  81 + var c = (a ?? "") + (b ?? "");
  82 + return c.Contains("退款完结") || c.Contains("已退款") || c.Contains("退款成功");
  83 + }
  84 +
  85 + // order_status 仍为待发货(2)或已发货(3)时,用主单/子单文案识别退款
  86 + if (orderStatus is 2 or 3)
  87 + {
  88 + if (DescRefundDone(orderStatusDesc, mainStatusDesc))
  89 + return 21;
  90 + if (DescRefundInProgress(orderStatusDesc, mainStatusDesc))
  91 + return 20;
  92 + if (skuList != null)
  93 + {
  94 + foreach (var sku in skuList)
  95 + {
  96 + if (sku is not JObject so) continue;
  97 + var sd = so["order_status_desc"]?.ToString() ?? "";
  98 + var smd = so["main_status_desc"]?.ToString() ?? "";
  99 + if (DescRefundDone(sd, smd)) return 21;
  100 + if (DescRefundInProgress(sd, smd)) return 20;
  101 + }
  102 + }
  103 + }
  104 +
  105 + // 原逻辑:已关闭(4)时从 main/sku/文案推断退款完结
  106 + if (orderStatus == 4)
  107 + {
  108 + if (mainStatus.HasValue && IsRefundFinished(mainStatus.Value))
  109 + return mainStatus.Value;
  110 + if (!DescExcludesRefund(orderStatusDesc, mainStatusDesc)
  111 + && (orderStatusDesc.Contains("退款") || mainStatusDesc.Contains("退款")))
  112 + return 21;
  113 + if (skuList != null)
  114 + {
  115 + foreach (var sku in skuList)
  116 + {
  117 + if (sku is not JObject so) continue;
  118 + var skuSt = so["order_status"]?.ToObject<int?>();
  119 + if (skuSt.HasValue && IsRefundFinished(skuSt.Value))
  120 + return skuSt.Value;
  121 + }
  122 + }
  123 + }
  124 +
  125 + return orderStatus;
  126 + }
  127 +
  128 + /// <summary>
27 /// 设置 Access Token(从授权回调获取) 129 /// 设置 Access Token(从授权回调获取)
28 /// </summary> 130 /// </summary>
29 public void SetAccessToken(string accessToken) 131 public void SetAccessToken(string accessToken)
@@ -215,44 +317,15 @@ public class DouyinService @@ -215,44 +317,15 @@ public class DouyinService
215 { 317 {
216 var orderId = orderObj["order_id"]?.ToString() ?? ""; 318 var orderId = orderObj["order_id"]?.ToString() ?? "";
217 319
218 - // 从 ShopOrderListItem 提取订单信息,优先识别已退款状态(21/22/39)  
219 - // 抖店可能将 order_status 返回为 4(已取消),但 main_status 或 sku 中为 21/22/39  
220 - var orderStatus = orderObj["order_status"]?.ToObject<int>() ?? 0;  
221 - var mainStatus = orderObj["main_status"]?.ToObject<int?>();  
222 - var orderStatusDesc = orderObj["order_status_desc"]?.ToString() ?? "";  
223 - var mainStatusDesc = orderObj["main_status_desc"]?.ToString() ?? "";  
224 -  
225 - // 若 order_status 为 4(已取消),检查 main_status 和 sku_order_list 是否为退款完结  
226 - if (orderStatus == 4)  
227 - {  
228 - var origStatus = orderStatus;  
229 - if (mainStatus.HasValue && (mainStatus.Value == 21 || mainStatus.Value == 22 || mainStatus.Value == 39))  
230 - orderStatus = mainStatus.Value;  
231 - else if (orderStatusDesc.Contains("退款") || mainStatusDesc.Contains("退款"))  
232 - orderStatus = 21; // 描述含退款则视为发货前退款完结  
233 - else  
234 - {  
235 - var skuList = orderObj["sku_order_list"] as Newtonsoft.Json.Linq.JArray;  
236 - if (skuList != null)  
237 - foreach (var sku in skuList)  
238 - if (sku is Newtonsoft.Json.Linq.JObject so)  
239 - {  
240 - var skuStatus = so["order_status"]?.ToObject<int?>();  
241 - if (skuStatus.HasValue && (skuStatus.Value == 21 || skuStatus.Value == 22 || skuStatus.Value == 39))  
242 - {  
243 - orderStatus = skuStatus.Value;  
244 - break;  
245 - }  
246 - }  
247 - }  
248 - if (orderStatus != origStatus)  
249 - _logger.LogInformation("订单 {OrderId} 状态修正: order_status={Orig}, main_status={Main}, 修正后={Final}", orderId, origStatus, mainStatus, orderStatus);  
250 - } 320 + var rawOrderStatus = orderObj["order_status"]?.ToObject<int>() ?? 0;
  321 + var orderStatus = ResolveRefundAwareOrderStatus(orderObj, rawOrderStatus);
  322 + if (orderStatus != rawOrderStatus)
  323 + _logger.LogInformation("订单 {OrderId} 状态修正: 原始 order_status={Raw}, 修正后={Final}", orderId, rawOrderStatus, orderStatus);
251 324
252 var order = new DouyinOrder 325 var order = new DouyinOrder
253 { 326 {
254 OrderId = orderId, 327 OrderId = orderId,
255 - OrderStatus = orderStatus, // 保存抖音的原始状态(含修正后的退款状态) 328 + OrderStatus = orderStatus, // 含 21/22/39 完结码与内部 20=退款处理中
256 OpenId = orderObj["open_id"]?.ToString() ?? orderObj["doudian_open_id"]?.ToString() 329 OpenId = orderObj["open_id"]?.ToString() ?? orderObj["doudian_open_id"]?.ToString()
257 }; 330 };
258 331
Antis.Erp.Plat/douyin/DouyinLogistics.API/Services/OrderService.cs
@@ -58,8 +58,8 @@ public class OrderService @@ -58,8 +58,8 @@ public class OrderService
58 58
59 /// <summary> 59 /// <summary>
60 /// 将抖音订单状态映射到本地订单状态 60 /// 将抖音订单状态映射到本地订单状态
61 - /// 抖音:2=待发货,3=已发货,21=发货前退款完结,22=发货后退款完结,39=收货后退款完结,4等=已取消  
62 - /// 本地:0=待发货,1=已发货,2=已取消,3=已退款 61 + /// 抖音:2=待发货,3=已发货,21/22/39=退款完结,4=已关闭;20=同步层识别的「退款处理中」(见 DouyinService)
  62 + /// 本地:0=待发货,1=已发货,2=已取消,3=已退款,4=退款中
63 /// </summary> 63 /// </summary>
64 private int MapOrderStatus(int douyinStatus) 64 private int MapOrderStatus(int douyinStatus)
65 { 65 {
@@ -67,6 +67,7 @@ public class OrderService @@ -67,6 +67,7 @@ public class OrderService
67 { 67 {
68 2 => 0, // 待发货(备货中) 68 2 => 0, // 待发货(备货中)
69 3 => 1, // 已发货 69 3 => 1, // 已发货
  70 + 20 => 4, // 退款处理中(抖音文案/main_status 识别,非官方数字码)
70 21 => 3, // 发货前退款完结 71 21 => 3, // 发货前退款完结
71 22 => 3, // 发货后退款完结 72 22 => 3, // 发货后退款完结
72 39 => 3, // 收货后退款完结 73 39 => 3, // 收货后退款完结
@@ -357,6 +358,13 @@ public class OrderService @@ -357,6 +358,13 @@ public class OrderService
357 { 358 {
358 try 359 try
359 { 360 {
  361 + if (order.Status == 2 || order.Status == 3 || order.Status == 4)
  362 + {
  363 + _logger.LogWarning("订单状态不允许创建运单: DbId={DbId}, OrderId={OrderId}, Status={Status}",
  364 + order.Id, order.OrderId, order.Status);
  365 + return false;
  366 + }
  367 +
360 _logger.LogInformation("开始为订单创建运单: OrderId={OrderId}, ReceiverName={ReceiverName}", 368 _logger.LogInformation("开始为订单创建运单: OrderId={OrderId}, ReceiverName={ReceiverName}",
361 order.OrderId, order.ReceiverName); 369 order.OrderId, order.ReceiverName);
362 370
@@ -449,6 +457,7 @@ public class OrderService @@ -449,6 +457,7 @@ public class OrderService
449 string? trackingNumber = null, 457 string? trackingNumber = null,
450 string? productName = null, 458 string? productName = null,
451 bool? hasWaybill = null, 459 bool? hasWaybill = null,
  460 + bool pendingShipmentForm = false,
452 DateTime? createTimeStart = null, 461 DateTime? createTimeStart = null,
453 DateTime? createTimeEnd = null, 462 DateTime? createTimeEnd = null,
454 DateTime? payTimeStart = null, 463 DateTime? payTimeStart = null,
@@ -486,24 +495,7 @@ public class OrderService @@ -486,24 +495,7 @@ public class OrderService
486 query = query.Where(o => o.ProductName != null && o.ProductName.Contains(productName)); 495 query = query.Where(o => o.ProductName != null && o.ProductName.Contains(productName));
487 } 496 }
488 497
489 - // 筛选是否有发货单  
490 - if (hasWaybill.HasValue)  
491 - {  
492 - if (hasWaybill.Value)  
493 - {  
494 - // 有发货单:存在waybills表中有该订单的记录  
495 - query = query.Where(o => SqlFunc.Subqueryable<Waybill>()  
496 - .Where(w => w.OrderId == o.Id)  
497 - .Any());  
498 - }  
499 - else  
500 - {  
501 - // 无发货单:waybills表中没有该订单的记录  
502 - query = query.Where(o => !SqlFunc.Subqueryable<Waybill>()  
503 - .Where(w => w.OrderId == o.Id)  
504 - .Any());  
505 - }  
506 - } 498 + // hasWaybill 在「同人同址合并」之后处理:否则只有子单有 waybills/运单号时,SQL 会整组拆散,列表里筛不出已建运单的合并单
507 499
508 if (createTimeStart.HasValue) 500 if (createTimeStart.HasValue)
509 { 501 {
@@ -525,12 +517,14 @@ public class OrderService @@ -525,12 +517,14 @@ public class OrderService
525 query = query.Where(o => o.PayTime != null && o.PayTime <= payTimeEnd.Value.AddDays(1).AddSeconds(-1)); 517 query = query.Where(o => o.PayTime != null && o.PayTime <= payTimeEnd.Value.AddDays(1).AddSeconds(-1));
526 } 518 }
527 519
528 - // 所有状态都参与合并(按买家+地址分组),已退款/已取消从合并组中剔除 520 + // 所有状态都参与合并(按买家+地址分组),已退款/退款中/已取消从合并组中剔除
529 var allOrders = await query 521 var allOrders = await query
530 .OrderBy($"IFNULL(PayTime, '1970-01-01 00:00:00') ASC, CreateTime ASC") 522 .OrderBy($"IFNULL(PayTime, '1970-01-01 00:00:00') ASC, CreateTime ASC")
531 .Take(1000) 523 .Take(1000)
532 .ToListAsync(); 524 .ToListAsync();
533 var merged = MergeOrdersByBuyerAndAddress(allOrders); 525 var merged = MergeOrdersByBuyerAndAddress(allOrders);
  526 + if (pendingShipmentForm || hasWaybill.HasValue)
  527 + merged = await FilterMergedOrdersPostProcessAsync(merged, hasWaybill, pendingShipmentForm);
534 var total = merged.Count; 528 var total = merged.Count;
535 var paged = merged 529 var paged = merged
536 .Skip((pageIndex - 1) * pageSize) 530 .Skip((pageIndex - 1) * pageSize)
@@ -543,7 +537,7 @@ public class OrderService @@ -543,7 +537,7 @@ public class OrderService
543 /// 合并订单规则: 537 /// 合并订单规则:
544 /// 1. 待发货(0):同买家同地址 → 合并(用于一起发货) 538 /// 1. 待发货(0):同买家同地址 → 合并(用于一起发货)
545 /// 2. 已发货(1):同运单号 → 合并(之前合并发货的继续合并展示,分开发货的分开展示) 539 /// 2. 已发货(1):同运单号 → 合并(之前合并发货的继续合并展示,分开发货的分开展示)
546 - /// 3. 已退款(3)中的订单从待发货合并组中剔除,单独显示 540 + /// 3. 已退款(3)、退款中(4)从待发货合并组中剔除,单独显示
547 /// 4. 已取消(2):始终单独显示,不合并 541 /// 4. 已取消(2):始终单独显示,不合并
548 /// </summary> 542 /// </summary>
549 private List<Order> MergeOrdersByBuyerAndAddress(List<Order> orders) 543 private List<Order> MergeOrdersByBuyerAndAddress(List<Order> orders)
@@ -557,12 +551,14 @@ public class OrderService @@ -557,12 +551,14 @@ public class OrderService
557 var shippedOrders = orders.Where(o => o.Status == 1).ToList(); // 已发货 551 var shippedOrders = orders.Where(o => o.Status == 1).ToList(); // 已发货
558 var cancelledOrders = orders.Where(o => o.Status == 2).ToList(); // 已取消 552 var cancelledOrders = orders.Where(o => o.Status == 2).ToList(); // 已取消
559 var refundedOrders = orders.Where(o => o.Status == 3).ToList(); // 已退款 553 var refundedOrders = orders.Where(o => o.Status == 3).ToList(); // 已退款
  554 + var refundingOrders = orders.Where(o => o.Status == 4).ToList(); // 退款中
560 555
561 // 规则4:已取消始终单独显示 556 // 规则4:已取消始终单独显示
562 result.AddRange(cancelledOrders); 557 result.AddRange(cancelledOrders);
563 558
564 - // 规则3:已退款始终单独显示 559 + // 规则3:已退款、退款中始终单独显示
565 result.AddRange(refundedOrders); 560 result.AddRange(refundedOrders);
  561 + result.AddRange(refundingOrders);
566 562
567 // 规则1:待发货按同买家同地址合并 563 // 规则1:待发货按同买家同地址合并
568 var pendingGroups = pendingOrders 564 var pendingGroups = pendingOrders
@@ -645,10 +641,79 @@ public class OrderService @@ -645,10 +641,79 @@ public class OrderService
645 first.MergedOrderStatuses = list.Select(o => o.Status).ToList(); 641 first.MergedOrderStatuses = list.Select(o => o.Status).ToList();
646 first.PayAmount = list.Sum(o => o.PayAmount ?? 0); 642 first.PayAmount = list.Sum(o => o.PayAmount ?? 0);
647 first.OrderAmount = list.Sum(o => o.OrderAmount ?? 0); 643 first.OrderAmount = list.Sum(o => o.OrderAmount ?? 0);
  644 + var withTracking = list.FirstOrDefault(o => !string.IsNullOrWhiteSpace(o.TrackingNumber));
  645 + if (withTracking != null)
  646 + {
  647 + first.TrackingNumber = withTracking.TrackingNumber;
  648 + if (!string.IsNullOrWhiteSpace(withTracking.LogisticsCompany))
  649 + first.LogisticsCompany = withTracking.LogisticsCompany;
  650 + }
648 return first; 651 return first;
649 } 652 }
650 653
651 /// <summary> 654 /// <summary>
  655 + /// 合并结果后筛选:hasWaybill 表示是否有运单/运单号;pendingShipmentForm 表示「有运单但未在系统中提交发货单」
  656 + /// (提交成功后会写入 waybills.SalesOrderId,即已生成 ERP 销售出库单)
  657 + /// </summary>
  658 + private async Task<List<Order>> FilterMergedOrdersPostProcessAsync(
  659 + List<Order> merged,
  660 + bool? hasWaybill,
  661 + bool pendingShipmentForm)
  662 + {
  663 + if (merged == null || merged.Count == 0)
  664 + return merged ?? new List<Order>();
  665 +
  666 + var allIds = merged
  667 + .SelectMany(m => m.MergedOrderIds != null && m.MergedOrderIds.Count > 0
  668 + ? m.MergedOrderIds
  669 + : new List<int> { m.Id })
  670 + .Distinct()
  671 + .ToList();
  672 +
  673 + var wbRows = await _db.Queryable<Waybill>()
  674 + .Where(w => allIds.Contains(w.OrderId))
  675 + .Select(w => new { w.OrderId, w.SalesOrderId })
  676 + .ToListAsync();
  677 + var hasWaybillRow = wbRows.Select(w => w.OrderId).ToHashSet();
  678 + var submittedOrderIds = wbRows
  679 + .Where(w => !string.IsNullOrWhiteSpace(w.SalesOrderId))
  680 + .Select(w => w.OrderId)
  681 + .ToHashSet();
  682 +
  683 + var trackRows = await _db.Queryable<Order>()
  684 + .Where(o => allIds.Contains(o.Id))
  685 + .Select(o => new { o.Id, o.TrackingNumber })
  686 + .ToListAsync();
  687 + var hasTracking = trackRows
  688 + .Where(t => !string.IsNullOrWhiteSpace(t.TrackingNumber))
  689 + .Select(t => t.Id)
  690 + .ToHashSet();
  691 +
  692 + bool GroupHasWaybill(IEnumerable<int> ids) =>
  693 + ids.Any(id => hasWaybillRow.Contains(id) || hasTracking.Contains(id));
  694 +
  695 + bool GroupHasSubmittedShipmentForm(IEnumerable<int> ids) =>
  696 + ids.Any(id => submittedOrderIds.Contains(id));
  697 +
  698 + return merged.Where(m =>
  699 + {
  700 + var ids = m.MergedOrderIds != null && m.MergedOrderIds.Count > 0
  701 + ? m.MergedOrderIds
  702 + : new List<int> { m.Id };
  703 + if (pendingShipmentForm)
  704 + {
  705 + return GroupHasWaybill(ids) && !GroupHasSubmittedShipmentForm(ids);
  706 + }
  707 + if (hasWaybill.HasValue)
  708 + {
  709 + var has = GroupHasWaybill(ids);
  710 + return hasWaybill.Value ? has : !has;
  711 + }
  712 + return true;
  713 + }).ToList();
  714 + }
  715 +
  716 + /// <summary>
652 /// 诊断订单合并情况(用于排查为何未合并) 717 /// 诊断订单合并情况(用于排查为何未合并)
653 /// </summary> 718 /// </summary>
654 public async Task<List<object>> GetMergeDiagnosisAsync(params string[] orderIds) 719 public async Task<List<object>> GetMergeDiagnosisAsync(params string[] orderIds)
@@ -778,6 +843,13 @@ public class OrderService @@ -778,6 +843,13 @@ public class OrderService
778 return result; 843 return result;
779 } 844 }
780 845
  846 + if (order.Status == 2 || order.Status == 3 || order.Status == 4)
  847 + {
  848 + result.ErrorMessage = "已取消、已退款或退款中的订单不允许打印";
  849 + result.Success = false;
  850 + return result;
  851 + }
  852 +
781 // 标记为已打印(实际打印由前端完成) 853 // 标记为已打印(实际打印由前端完成)
782 result.Printed = true; 854 result.Printed = true;
783 result.Shipped = false; // 不执行发货操作 855 result.Shipped = false; // 不执行发货操作
@@ -835,6 +907,12 @@ public class OrderService @@ -835,6 +907,12 @@ public class OrderService
835 return result; 907 return result;
836 } 908 }
837 909
  910 + if (order.Status == 2 || order.Status == 3 || order.Status == 4)
  911 + {
  912 + result.ErrorMessage = "已取消、已退款或退款中的订单不允许发货";
  913 + return result;
  914 + }
  915 +
838 // 如果订单状态还是待发货,则发货到抖音 916 // 如果订单状态还是待发货,则发货到抖音
839 if (order.Status == 0) // 待发货 917 if (order.Status == 0) // 待发货
840 { 918 {
@@ -915,6 +993,12 @@ public class OrderService @@ -915,6 +993,12 @@ public class OrderService
915 return result; 993 return result;
916 } 994 }
917 995
  996 + if (order.Status == 2 || order.Status == 3 || order.Status == 4)
  997 + {
  998 + result.ErrorMessage = "已取消、已退款或退款中的订单不允许打印并发货";
  999 + return result;
  1000 + }
  1001 +
918 // 标记为已打印(这里只是标记,实际打印由前端完成) 1002 // 标记为已打印(这里只是标记,实际打印由前端完成)
919 result.Printed = true; 1003 result.Printed = true;
920 result.TrackingNumber = order.TrackingNumber; 1004 result.TrackingNumber = order.TrackingNumber;
Antis.Erp.Plat/douyin/DouyinLogistics.API/appsettings.Development.json
@@ -4,5 +4,8 @@ @@ -4,5 +4,8 @@
4 "Default": "Information", 4 "Default": "Information",
5 "Microsoft.AspNetCore": "Warning" 5 "Microsoft.AspNetCore": "Warning"
6 } 6 }
  7 + },
  8 + "ErpApi": {
  9 + "BaseUrl": "http://localhost:2011"
7 } 10 }
8 } 11 }
Antis.Erp.Plat/douyin/frontend/src/api/order.ts
@@ -15,6 +15,7 @@ const api = axios.create({ @@ -15,6 +15,7 @@ const api = axios.create({
15 export interface Order { 15 export interface Order {
16 id: number 16 id: number
17 orderId: string 17 orderId: string
  18 + /** 0 待发货 1 已发货 2 已取消 3 已退款 4 退款中 */
18 status: number 19 status: number
19 /** 合并的订单ID列表(同人同地址合并时) */ 20 /** 合并的订单ID列表(同人同地址合并时) */
20 mergedOrderIds?: number[] 21 mergedOrderIds?: number[]
@@ -65,6 +66,8 @@ export const getOrders = ( @@ -65,6 +66,8 @@ export const getOrders = (
65 trackingNumber?: string 66 trackingNumber?: string
66 productName?: string 67 productName?: string
67 hasWaybill?: boolean 68 hasWaybill?: boolean
  69 + /** 有运单但未提交发货单(未生成 ERP 出库单,后端用 waybills.SalesOrderId 判断) */
  70 + pendingShipmentForm?: boolean
68 createTimeStart?: string 71 createTimeStart?: string
69 createTimeEnd?: string 72 createTimeEnd?: string
70 payTimeStart?: string 73 payTimeStart?: string
@@ -80,6 +83,7 @@ export const getOrders = ( @@ -80,6 +83,7 @@ export const getOrders = (
80 if (filters.trackingNumber) params.trackingNumber = filters.trackingNumber 83 if (filters.trackingNumber) params.trackingNumber = filters.trackingNumber
81 if (filters.productName) params.productName = filters.productName 84 if (filters.productName) params.productName = filters.productName
82 if (filters.hasWaybill !== undefined) params.hasWaybill = filters.hasWaybill 85 if (filters.hasWaybill !== undefined) params.hasWaybill = filters.hasWaybill
  86 + if (filters.pendingShipmentForm !== undefined) params.pendingShipmentForm = filters.pendingShipmentForm
83 if (filters.createTimeStart) params.createTimeStart = filters.createTimeStart 87 if (filters.createTimeStart) params.createTimeStart = filters.createTimeStart
84 if (filters.createTimeEnd) params.createTimeEnd = filters.createTimeEnd 88 if (filters.createTimeEnd) params.createTimeEnd = filters.createTimeEnd
85 if (filters.payTimeStart) params.payTimeStart = filters.payTimeStart 89 if (filters.payTimeStart) params.payTimeStart = filters.payTimeStart
@@ -152,7 +156,7 @@ export const getMergedOrderDetail = (ids: number[]) =&gt; { @@ -152,7 +156,7 @@ export const getMergedOrderDetail = (ids: number[]) =&gt; {
152 return api.get('/orders/detail/merged', { params: { ids: ids.join(',') } }) 156 return api.get('/orders/detail/merged', { params: { ids: ids.join(',') } })
153 } 157 }
154 158
155 -// 查询ERP商品列表 159 +// 查询ERP商品列表(pl:单个品类 F_Id;ERP 商品可挂多品类逗号存储,后端按「包含该 ID」匹配)
156 export const searchProducts = (keyword?: string, pageIndex = 1, pageSize = 20, pl?: string) => { 160 export const searchProducts = (keyword?: string, pageIndex = 1, pageSize = 20, pl?: string) => {
157 const params: any = { pageIndex, pageSize } 161 const params: any = { pageIndex, pageSize }
158 if (keyword) params.keyword = keyword 162 if (keyword) params.keyword = keyword
Antis.Erp.Plat/douyin/frontend/src/utils/config.ts
@@ -48,3 +48,14 @@ export const getBackendBaseUrl = (): string =&gt; { @@ -48,3 +48,14 @@ export const getBackendBaseUrl = (): string =&gt; {
48 return window.location.origin 48 return window.location.origin
49 } 49 }
50 50
  51 +/** ERP(NCC) 站点根地址,用于拼接商品主图等相对路径(须与 DouyinLogistics.API 的 ErpApi.BaseUrl 一致) */
  52 +export const getErpBaseUrl = (): string => {
  53 + if (import.meta.env.VITE_ERP_BASE_URL) {
  54 + return String(import.meta.env.VITE_ERP_BASE_URL).replace(/\/$/, '')
  55 + }
  56 + if (import.meta.env.DEV) {
  57 + return 'http://localhost:2011'
  58 + }
  59 + return 'http://localhost:2011'
  60 +}
  61 +
Antis.Erp.Plat/douyin/frontend/src/views/CreateWaybillView.vue
@@ -9,7 +9,7 @@ @@ -9,7 +9,7 @@
9 type="primary" 9 type="primary"
10 @click="handleSubmit" 10 @click="handleSubmit"
11 :loading="submitting" 11 :loading="submitting"
12 - :disabled="(waybillStatus !== undefined && waybillStatus >= 1) || form.status === 2 || form.status === 3" 12 + :disabled="(waybillStatus !== undefined && waybillStatus >= 1) || form.status === 2 || form.status === 3 || form.status === 4"
13 size="small" 13 size="small"
14 > 14 >
15 提交发货单 15 提交发货单
@@ -36,17 +36,30 @@ @@ -36,17 +36,30 @@
36 </el-col> 36 </el-col>
37 <el-col :span="24"> 37 <el-col :span="24">
38 <el-form-item label="订单状态"> 38 <el-form-item label="订单状态">
39 - <el-tag :type="form.status === 0 ? 'warning' : form.status === 1 ? 'success' : form.status === 3 ? 'info' : 'info'" size="small">  
40 - {{ form.status === 0 ? '待发货' : form.status === 1 ? '已发货' : form.status === 3 ? '已退款' : '已取消' }} 39 + <el-tag
  40 + :type="form.status === 0 ? 'warning' : form.status === 1 ? 'success' : form.status === 3 ? 'info' : form.status === 4 ? 'warning' : 'info'"
  41 + size="small"
  42 + >
  43 + {{
  44 + form.status === 0
  45 + ? '待发货'
  46 + : form.status === 1
  47 + ? '已发货'
  48 + : form.status === 3
  49 + ? '已退款'
  50 + : form.status === 4
  51 + ? '退款中'
  52 + : '已取消'
  53 + }}
41 </el-tag> 54 </el-tag>
42 <el-alert 55 <el-alert
43 - v-if="form.status === 2 || form.status === 3" 56 + v-if="form.status === 2 || form.status === 3 || form.status === 4"
44 type="error" 57 type="error"
45 :closable="false" 58 :closable="false"
46 show-icon 59 show-icon
47 style="margin-top: 8px;" 60 style="margin-top: 8px;"
48 > 61 >
49 - 已取消或已退款的订单不允许进行任何操作 62 + 已取消、已退款或退款中的订单不允许进行任何操作
50 </el-alert> 63 </el-alert>
51 <el-alert 64 <el-alert
52 v-else-if="waybillStatus !== undefined && waybillStatus >= 1" 65 v-else-if="waybillStatus !== undefined && waybillStatus >= 1"
@@ -289,7 +302,7 @@ @@ -289,7 +302,7 @@
289 {{ getSerialNumberTypeText(row) }} 302 {{ getSerialNumberTypeText(row) }}
290 </el-tag> 303 </el-tag>
291 <el-icon 304 <el-icon
292 - v-if="row.spbm" 305 + v-if="row.spbm || getSkuCode(row)"
293 style="cursor: pointer; color: #409eff;" 306 style="cursor: pointer; color: #409eff;"
294 @click="refreshSerialNumberType(row)" 307 @click="refreshSerialNumberType(row)"
295 :title="'刷新序列号类型信息'" 308 :title="'刷新序列号类型信息'"
@@ -442,6 +455,7 @@ import { useRoute, useRouter } from &#39;vue-router&#39; @@ -442,6 +455,7 @@ import { useRoute, useRouter } from &#39;vue-router&#39;
442 import { ElMessage, ElMessageBox } from 'element-plus' 455 import { ElMessage, ElMessageBox } from 'element-plus'
443 import { Refresh } from '@element-plus/icons-vue' 456 import { Refresh } from '@element-plus/icons-vue'
444 import { getOrderDetail, getMergedOrderDetail, searchProducts as searchProductsAPI, manualCreateWaybill, getProductInfo as getProductInfoAPI, createSalesOrder, getWarehouses, getProductCategories, updateSellerRemark, checkStock, getDefaults } from '@/api/order' 457 import { getOrderDetail, getMergedOrderDetail, searchProducts as searchProductsAPI, manualCreateWaybill, getProductInfo as getProductInfoAPI, createSalesOrder, getWarehouses, getProductCategories, updateSellerRemark, checkStock, getDefaults } from '@/api/order'
  458 +import { getErpBaseUrl } from '@/utils/config'
445 import SerialNumberSelect from '@/components/SerialNumberSelect.vue' 459 import SerialNumberSelect from '@/components/SerialNumberSelect.vue'
446 import axios from 'axios' 460 import axios from 'axios'
447 461
@@ -458,6 +472,17 @@ const serialNumberSelectRef = ref() @@ -458,6 +472,17 @@ const serialNumberSelectRef = ref()
458 // 商品信息缓存(用于存储序列号类型) 472 // 商品信息缓存(用于存储序列号类型)
459 const productCache = new Map<string, any>() 473 const productCache = new Map<string, any>()
460 474
  475 +/** 商品主图:补全 ERP 相对路径,避免 el-image 请求失败 */
  476 +const resolveProductPicUrl = (pic: string | undefined | null): string => {
  477 + const s = (pic || '').trim()
  478 + if (!s) return ''
  479 + if (/^https?:\/\//i.test(s)) return s
  480 + if (s.startsWith('//')) return `https:${s}`
  481 + const base = getErpBaseUrl().replace(/\/$/, '')
  482 + if (s.startsWith('/')) return `${base}${s}`
  483 + return s
  484 +}
  485 +
461 // 注意:所有商品信息查询都通过Douyin API代理,避免CORS问题 486 // 注意:所有商品信息查询都通过Douyin API代理,避免CORS问题
462 // 不再直接访问主ERP系统 487 // 不再直接访问主ERP系统
463 488
@@ -602,9 +627,10 @@ const loadOrderDetail = async () =&gt; { @@ -602,9 +627,10 @@ const loadOrderDetail = async () =&gt; {
602 } 627 }
603 628
604 // 尝试多种可能的字段名 629 // 尝试多种可能的字段名
  630 + const rawPic = item.product_pic || item.ProductPic || item.pic || item.image || ''
605 const mappedItem = { 631 const mappedItem = {
606 product_name: item.product_name || item.ProductName || item.spmc || item.Spmc || item.name || '', 632 product_name: item.product_name || item.ProductName || item.spmc || item.Spmc || item.name || '',
607 - product_pic: item.product_pic || item.ProductPic || item.pic || item.image || '', 633 + product_pic: resolveProductPicUrl(rawPic),
608 item_num: item.item_num || item.ItemNum || item.itemNum || item.quantity || item.num || 1, 634 item_num: item.item_num || item.ItemNum || item.itemNum || item.quantity || item.num || 1,
609 goods_price: goodsPrice, 635 goods_price: goodsPrice,
610 spec: item.spec || item.Spec || item.gg || item.Gg || item.specification || '', 636 spec: item.spec || item.Spec || item.gg || item.Gg || item.specification || '',
@@ -626,33 +652,7 @@ const loadOrderDetail = async () =&gt; { @@ -626,33 +652,7 @@ const loadOrderDetail = async () =&gt; {
626 }) 652 })
627 console.log('最终商品清单:', form.value.productItems) 653 console.log('最终商品清单:', form.value.productItems)
628 654
629 - // 确保所有商品的序列号类型都已加载,并且spbm字段已正确设置  
630 - await Promise.all(  
631 - form.value.productItems.map(async (item: any) => {  
632 - // 如果没有spbm但有sku_id,先通过sku_id查找spbm  
633 - if (!item.spbm && getSkuCode(item)) {  
634 - const skuId = getSkuCode(item)  
635 - console.log('商品缺少spbm,通过sku_id查找:', { skuId, item })  
636 - try {  
637 - const response = await searchProductsAPI(skuId, 1, 1)  
638 - if (response.data && response.data.data && response.data.data.length > 0) {  
639 - const product = response.data.data[0]  
640 - if (product.spbm) {  
641 - item.spbm = product.spbm  
642 - console.log('通过sku_id找到spbm:', { skuId, spbm: product.spbm })  
643 - }  
644 - }  
645 - } catch (error) {  
646 - console.error('通过sku_id查找spbm失败:', error)  
647 - }  
648 - }  
649 -  
650 - // 加载序列号类型  
651 - if (item.spbm || getSkuCode(item)) {  
652 - await loadSerialNumberType(item)  
653 - }  
654 - })  
655 - ) 655 + await Promise.all(form.value.productItems.map((item: any) => loadSerialNumberType(item)))
656 } else { 656 } else {
657 console.log('没有商品清单数据') 657 console.log('没有商品清单数据')
658 form.value.productItems = [] 658 form.value.productItems = []
@@ -733,7 +733,7 @@ const addProduct = async (product: any) =&gt; { @@ -733,7 +733,7 @@ const addProduct = async (product: any) =&gt; {
733 const images = typeof product.spzt === 'string' ? JSON.parse(product.spzt) : product.spzt 733 const images = typeof product.spzt === 'string' ? JSON.parse(product.spzt) : product.spzt
734 if (Array.isArray(images) && images.length > 0 && images[0].url) { 734 if (Array.isArray(images) && images.length > 0 && images[0].url) {
735 const url = images[0].url 735 const url = images[0].url
736 - productPic = url.startsWith('http') ? url : `http://localhost:2011${url}` 736 + productPic = url.startsWith('http') ? url : `${getErpBaseUrl()}${url.startsWith('/') ? '' : '/'}${url}`
737 } 737 }
738 } catch (e) { 738 } catch (e) {
739 console.warn('解析商品主图失败:', e) 739 console.warn('解析商品主图失败:', e)
@@ -743,7 +743,7 @@ const addProduct = async (product: any) =&gt; { @@ -743,7 +743,7 @@ const addProduct = async (product: any) =&gt; {
743 // 添加商品到清单 743 // 添加商品到清单
744 const newProduct = { 744 const newProduct = {
745 product_name: product.spmc || '', 745 product_name: product.spmc || '',
746 - product_pic: productPic, 746 + product_pic: resolveProductPicUrl(productPic || product.imageUrl || ''),
747 spbm: product.spbm || '', 747 spbm: product.spbm || '',
748 sku_id: product.dyspid || '', 748 sku_id: product.dyspid || '',
749 item_num: 1, 749 item_num: 1,
@@ -804,32 +804,31 @@ const removeProduct = (index: number) =&gt; { @@ -804,32 +804,31 @@ const removeProduct = (index: number) =&gt; {
804 form.value.productItems.splice(index, 1) 804 form.value.productItems.splice(index, 1)
805 } 805 }
806 806
807 -// 获取商品信息(用于获取序列号类型)  
808 -// 通过Douyin API代理请求,避免CORS问题  
809 -const getProductInfo = async (productCode: string) => {  
810 - const key = String(productCode)  
811 - // 如果缓存中已有该商品信息,直接返回  
812 - if (productCache.has(key)) {  
813 - return productCache.get(key) 807 +// 获取商品信息(用于获取序列号类型);可按商品编码或抖音 SKU 查询
  808 +const getProductInfo = async (productCode?: string, skuId?: string) => {
  809 + const code = productCode ? String(productCode).trim() : ''
  810 + const sku = skuId ? String(skuId).trim() : ''
  811 + const cacheKey = code ? code : sku ? `sku:${sku}` : ''
  812 + if (!cacheKey) return null
  813 + if (productCache.has(cacheKey)) {
  814 + return productCache.get(cacheKey)
814 } 815 }
815 816
816 try { 817 try {
817 - // 通过Douyin API代理请求,避免CORS问题  
818 - console.log('查询商品信息 - 商品编码:', key)  
819 - const response = await getProductInfoAPI(key)  
820 -  
821 - console.log('查询商品信息 - 响应:', response.data)  
822 - 818 + console.log('查询商品信息 - spbm:', code || '(无)', 'skuId:', sku || '(无)')
  819 + const response = await getProductInfoAPI(code || undefined, sku || undefined)
  820 +
823 if (response.data && response.data.code === 200 && response.data.data) { 821 if (response.data && response.data.code === 200 && response.data.data) {
824 const product = response.data.data 822 const product = response.data.data
825 - product.spxlhType = String(product.spxlhType || '')  
826 - productCache.set(key, product)  
827 - console.log('获取商品信息成功:', { productCode: key, spxlhType: product.spxlhType, productName: product.spmc }) 823 + product.spxlhType = product.spxlhType != null && product.spxlhType !== '' ? String(product.spxlhType) : ''
  824 + productCache.set(cacheKey, product)
  825 + if (product.spbm) {
  826 + productCache.set(String(product.spbm), product)
  827 + }
  828 + console.log('获取商品信息成功:', { cacheKey, spxlhType: product.spxlhType, productName: product.spmc })
828 return product 829 return product
829 - } else {  
830 - console.warn('未找到商品信息:', key, response.data)  
831 } 830 }
832 - 831 + console.warn('未找到商品信息:', cacheKey, response.data)
833 return null 832 return null
834 } catch (error) { 833 } catch (error) {
835 console.error('获取商品信息失败:', error) 834 console.error('获取商品信息失败:', error)
@@ -837,54 +836,52 @@ const getProductInfo = async (productCode: string) =&gt; { @@ -837,54 +836,52 @@ const getProductInfo = async (productCode: string) =&gt; {
837 } 836 }
838 } 837 }
839 838
840 -// 加载序列号类型 839 +// 加载序列号类型(始终在 finally 里标记 spxlhLoaded,避免一直显示「加载中」)
841 const loadSerialNumberType = async (item: any) => { 840 const loadSerialNumberType = async (item: any) => {
842 - // 如果没有spbm,尝试通过sku_id查找  
843 - if (!item.spbm) { 841 + item.spxlhLoaded = false
  842 + try {
844 const skuId = getSkuCode(item) 843 const skuId = getSkuCode(item)
845 - if (skuId) { 844 +
  845 + if (!item.spbm && skuId) {
  846 + const bySku = await getProductInfo(undefined, skuId)
  847 + if (bySku) {
  848 + if (bySku.spbm) item.spbm = String(bySku.spbm)
  849 + if (bySku.spxlhType !== undefined && bySku.spxlhType !== null && String(bySku.spxlhType) !== '') {
  850 + item.spxlhType = String(bySku.spxlhType)
  851 + }
  852 + if (bySku.imageUrl) {
  853 + item.product_pic = resolveProductPicUrl(bySku.imageUrl)
  854 + }
  855 + }
  856 + }
  857 +
  858 + if (!item.spbm && skuId) {
846 try { 859 try {
847 - // 通过sku_id查找对应的ERP商品(使用Douyin API代理)  
848 const response = await searchProductsAPI(skuId, 1, 1) 860 const response = await searchProductsAPI(skuId, 1, 1)
849 -  
850 - if (response.data && response.data.data && response.data.data.length > 0) { 861 + if (response.data?.data?.length > 0) {
851 const product = response.data.data[0] 862 const product = response.data.data[0]
852 - if (product.spbm) {  
853 - // 更新item的spbm字段  
854 - item.spbm = product.spbm  
855 - console.log('通过sku_id找到商品编码:', { skuId, spbm: product.spbm, product })  
856 - // 继续加载序列号类型  
857 - } else {  
858 - console.warn('通过sku_id查找商品,但未找到spbm:', { skuId, product })  
859 - return 863 + if (product.spbm) item.spbm = product.spbm
  864 + if (product.imageUrl) {
  865 + item.product_pic = resolveProductPicUrl(product.imageUrl)
860 } 866 }
861 - } else {  
862 - console.warn('通过sku_id未找到商品:', skuId)  
863 - return  
864 } 867 }
865 } catch (error) { 868 } catch (error) {
866 - console.error('通过sku_id查找商品编码失败:', error)  
867 - return 869 + console.error('通过 sku 搜索商品失败:', error)
868 } 870 }
869 - } else {  
870 - console.warn('商品没有spbm也没有sku_id:', item)  
871 - return  
872 } 871 }
873 - }  
874 -  
875 - if (!item.spbm) {  
876 - console.warn('商品没有spbm,无法加载序列号类型:', item)  
877 - return  
878 - }  
879 -  
880 - item.spxlhLoaded = false  
881 - try {  
882 - const product = await getProductInfo(item.spbm)  
883 - if (product && product.spxlhType) {  
884 - item.spxlhType = String(product.spxlhType)  
885 - console.log('序列号类型加载成功:', { spbm: item.spbm, spxlhType: item.spxlhType })  
886 - } else {  
887 - console.warn('未获取到序列号类型:', { spbm: item.spbm, product }) 872 +
  873 + if (item.spbm) {
  874 + const product = await getProductInfo(item.spbm)
  875 + if (product) {
  876 + if (product.spxlhType !== undefined && product.spxlhType !== null && String(product.spxlhType) !== '') {
  877 + item.spxlhType = String(product.spxlhType)
  878 + }
  879 + if (product.imageUrl) {
  880 + item.product_pic = resolveProductPicUrl(product.imageUrl)
  881 + }
  882 + }
  883 + } else if (!skuId) {
  884 + console.warn('商品没有 spbm 也没有 sku,跳过序列号类型:', item)
888 } 885 }
889 } catch (error) { 886 } catch (error) {
890 console.error('加载序列号类型失败:', error) 887 console.error('加载序列号类型失败:', error)
@@ -943,11 +940,10 @@ const refreshSerialNumberType = async (row: any) =&gt; { @@ -943,11 +940,10 @@ const refreshSerialNumberType = async (row: any) =&gt; {
943 } 940 }
944 } 941 }
945 942
946 - // 清除缓存  
947 - const key = String(row.spbm)  
948 - productCache.delete(key)  
949 -  
950 - // 重新加载 943 + if (row.spbm) productCache.delete(String(row.spbm))
  944 + const sku = getSkuCode(row)
  945 + if (sku) productCache.delete(`sku:${sku}`)
  946 +
951 await loadSerialNumberType(row) 947 await loadSerialNumberType(row)
952 ElMessage.success('序列号类型已刷新') 948 ElMessage.success('序列号类型已刷新')
953 } catch (error) { 949 } catch (error) {
@@ -1260,6 +1256,11 @@ const handleSubmit = async () =&gt; { @@ -1260,6 +1256,11 @@ const handleSubmit = async () =&gt; {
1260 ElMessage.error(`该发货单已${statusText},不允许重复提交`) 1256 ElMessage.error(`该发货单已${statusText},不允许重复提交`)
1261 return 1257 return
1262 } 1258 }
  1259 +
  1260 + if (form.value.status === 2 || form.value.status === 3 || form.value.status === 4) {
  1261 + ElMessage.error('已取消、已退款或退款中的订单不允许提交发货单')
  1262 + return
  1263 + }
1263 1264
1264 // 验证必填字段 1265 // 验证必填字段
1265 if (!form.value.cjck) { 1266 if (!form.value.cjck) {
Antis.Erp.Plat/douyin/frontend/src/views/OrderListView.vue
@@ -18,10 +18,11 @@ @@ -18,10 +18,11 @@
18 <el-select v-model="filterForm.status" placeholder="全部" clearable style="width: 150px"> 18 <el-select v-model="filterForm.status" placeholder="全部" clearable style="width: 150px">
19 <el-option label="全部" :value="undefined" /> 19 <el-option label="全部" :value="undefined" />
20 <el-option label="待发货" :value="0" /> 20 <el-option label="待发货" :value="0" />
21 - <el-option label="已打印 未发货" value="printed_not_shipped" /> 21 + <el-option label="有运单·未提交发货单" value="printed_not_shipped" />
22 <el-option label="已发货" :value="1" /> 22 <el-option label="已发货" :value="1" />
23 <el-option label="已取消" :value="2" /> 23 <el-option label="已取消" :value="2" />
24 <el-option label="已退款" :value="3" /> 24 <el-option label="已退款" :value="3" />
  25 + <el-option label="退款中" :value="4" />
25 </el-select> 26 </el-select>
26 </el-form-item> 27 </el-form-item>
27 <el-form-item label="订单编号"> 28 <el-form-item label="订单编号">
@@ -282,7 +283,7 @@ @@ -282,7 +283,7 @@
282 type="primary" 283 type="primary"
283 size="small" 284 size="small"
284 @click="handleCreateWaybill(row)" 285 @click="handleCreateWaybill(row)"
285 - :disabled="row.status === 2" 286 + :disabled="row.status === 2 || row.status === 3 || row.status === 4"
286 > 287 >
287 创建运单 288 创建运单
288 </el-button> 289 </el-button>
@@ -339,7 +340,7 @@ const batchLoading = ref(false) @@ -339,7 +340,7 @@ const batchLoading = ref(false)
339 const orders = ref<Order[]>([]) 340 const orders = ref<Order[]>([])
340 const selectedOrders = ref<Order[]>([]) 341 const selectedOrders = ref<Order[]>([])
341 const filterForm = ref({ 342 const filterForm = ref({
342 - status: 0 as number | string | undefined, // 默认显示待发货订单,支持特殊字符串 'printed_not_shipped' 343 + status: 0 as number | string | undefined, // 默认待发货;'printed_not_shipped' → 有运单且未提交发货单(未写 SalesOrderId)
343 orderId: '', 344 orderId: '',
344 receiverName: '', 345 receiverName: '',
345 receiverPhone: '', 346 receiverPhone: '',
@@ -363,7 +364,7 @@ const fetchOrders = async () =&gt; { @@ -363,7 +364,7 @@ const fetchOrders = async () =&gt; {
363 364
364 if (filterForm.value.status === 'printed_not_shipped') { 365 if (filterForm.value.status === 'printed_not_shipped') {
365 filters.status = 0 366 filters.status = 0
366 - filters.hasWaybill = true 367 + filters.pendingShipmentForm = true
367 } else if (filterForm.value.status !== undefined) { 368 } else if (filterForm.value.status !== undefined) {
368 filters.status = filterForm.value.status 369 filters.status = filterForm.value.status
369 } 370 }
@@ -623,10 +624,10 @@ const handleBatchCreateWaybill = async () =&gt; { @@ -623,10 +624,10 @@ const handleBatchCreateWaybill = async () =&gt; {
623 624
624 // 过滤掉已取消、已退款的订单 625 // 过滤掉已取消、已退款的订单
625 const validOrders = selectedOrders.value.filter((order: Order) => order.status === 0 || order.status === 1) 626 const validOrders = selectedOrders.value.filter((order: Order) => order.status === 0 || order.status === 1)
626 - const invalidOrders = selectedOrders.value.filter((order: Order) => order.status === 2 || order.status === 3) 627 + const invalidOrders = selectedOrders.value.filter((order: Order) => order.status === 2 || order.status === 3 || order.status === 4)
627 628
628 if (invalidOrders.length > 0) { 629 if (invalidOrders.length > 0) {
629 - ElMessage.warning(`已过滤 ${invalidOrders.length} 个已取消/已退款的订单,不允许创建运单`) 630 + ElMessage.warning(`已过滤 ${invalidOrders.length} 个已取消/已退款/退款中的订单,不允许创建运单`)
630 } 631 }
631 632
632 if (validOrders.length === 0) { 633 if (validOrders.length === 0) {
@@ -719,6 +720,10 @@ const handleBatchCreateWaybill = async () =&gt; { @@ -719,6 +720,10 @@ const handleBatchCreateWaybill = async () =&gt; {
719 720
720 // 创建运单(合并单跳转到合并编辑页;单笔订单走原有流程) 721 // 创建运单(合并单跳转到合并编辑页;单笔订单走原有流程)
721 const handleCreateWaybill = async (order: Order) => { 722 const handleCreateWaybill = async (order: Order) => {
  723 + if (order.status === 2 || order.status === 3 || order.status === 4) {
  724 + ElMessage.warning('已取消、已退款或退款中的订单不允许创建运单')
  725 + return
  726 + }
722 if (order.mergedOrderIds && order.mergedOrderIds.length > 1) { 727 if (order.mergedOrderIds && order.mergedOrderIds.length > 1) {
723 handleManualCreateWaybill(order) 728 handleManualCreateWaybill(order)
724 return 729 return
@@ -898,7 +903,8 @@ const getStatusText = (status: number) =&gt; { @@ -898,7 +903,8 @@ const getStatusText = (status: number) =&gt; {
898 0: '待发货', 903 0: '待发货',
899 1: '已发货', 904 1: '已发货',
900 2: '已取消', 905 2: '已取消',
901 - 3: '已退款' 906 + 3: '已退款',
  907 + 4: '退款中'
902 } 908 }
903 return statusMap[status] || '未知' 909 return statusMap[status] || '未知'
904 } 910 }
@@ -909,7 +915,8 @@ const getStatusType = (status: number) =&gt; { @@ -909,7 +915,8 @@ const getStatusType = (status: number) =&gt; {
909 0: 'info', 915 0: 'info',
910 1: 'success', 916 1: 'success',
911 2: 'danger', 917 2: 'danger',
912 - 3: 'warning' 918 + 3: 'warning',
  919 + 4: 'warning'
913 } 920 }
914 return typeMap[status] || 'info' 921 return typeMap[status] || 'info'
915 } 922 }
Antis.Erp.Plat/netcore/src/Application/NCC.API/Properties/launchSettings.json
1 -{ 1 +{
2 "iisSettings": { 2 "iisSettings": {
3 "windowsAuthentication": false, 3 "windowsAuthentication": false,
4 "anonymousAuthentication": true, 4 "anonymousAuthentication": true,
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtSpCrInput.cs
@@ -15,7 +15,7 @@ namespace NCC.Extend.Entitys.Dto.WtSp @@ -15,7 +15,7 @@ namespace NCC.Extend.Entitys.Dto.WtSp
15 public string spmc { get; set; } 15 public string spmc { get; set; }
16 16
17 /// <summary> 17 /// <summary>
18 - /// 商品品类 18 + /// 商品品类(多个 wt_pl.F_Id 用英文逗号分隔,如 id1,id2)
19 /// </summary> 19 /// </summary>
20 public string pl { get; set; } 20 public string pl { get; set; }
21 21
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtSpService.cs
@@ -51,6 +51,16 @@ namespace NCC.Extend.WtSp @@ -51,6 +51,16 @@ namespace NCC.Extend.WtSp
51 } 51 }
52 52
53 /// <summary> 53 /// <summary>
  54 + /// 按品类筛选:F_Pl 为单个 ID 或逗号分隔多 ID 时,均用 MySQL FIND_IN_SET 匹配(避免 LINQ Contains/StartsWith 在部分环境下对多值字段翻译异常)。
  55 + /// </summary>
  56 + private static ISugarQueryable<WtSpEntity> WherePlCategoryIfAny(ISugarQueryable<WtSpEntity> query, string pl)
  57 + {
  58 + var id = pl?.Trim();
  59 + if (string.IsNullOrEmpty(id)) return query;
  60 + return query.Where("FIND_IN_SET(@spPlCategory,`F_Pl`)>0", new { spPlCategory = id });
  61 + }
  62 +
  63 + /// <summary>
54 /// 获取商品档案 64 /// 获取商品档案
55 /// </summary> 65 /// </summary>
56 /// <param name="id">参数</param> 66 /// <param name="id">参数</param>
@@ -84,9 +94,8 @@ namespace NCC.Extend.WtSp @@ -84,9 +94,8 @@ namespace NCC.Extend.WtSp
84 List<object> queryKc = input.kc != null ? input.kc.Split(',').ToObeject<List<object>>() : null; 94 List<object> queryKc = input.kc != null ? input.kc.Split(',').ToObeject<List<object>>() : null;
85 var startKc = input.kc != null && !string.IsNullOrEmpty(queryKc.First().ToString()) ? queryKc.First() : decimal.MinValue; 95 var startKc = input.kc != null && !string.IsNullOrEmpty(queryKc.First().ToString()) ? queryKc.First() : decimal.MinValue;
86 var endKc = input.kc != null && !string.IsNullOrEmpty(queryKc.Last().ToString()) ? queryKc.Last() : decimal.MaxValue; 96 var endKc = input.kc != null && !string.IsNullOrEmpty(queryKc.Last().ToString()) ? queryKc.Last() : decimal.MaxValue;
87 - var data = await _db.Queryable<WtSpEntity>()  
88 - .WhereIF(!string.IsNullOrEmpty(input.spmc), p => p.Spmc.Contains(input.spmc))  
89 - .WhereIF(!string.IsNullOrEmpty(input.pl), p => p.Pl.Equals(input.pl)) 97 + var data = await WherePlCategoryIfAny(_db.Queryable<WtSpEntity>()
  98 + .WhereIF(!string.IsNullOrEmpty(input.spmc), p => p.Spmc.Contains(input.spmc)), input.pl)
90 .WhereIF(!string.IsNullOrEmpty(input.pp), p => p.Pp.Equals(input.pp)) 99 .WhereIF(!string.IsNullOrEmpty(input.pp), p => p.Pp.Equals(input.pp))
91 .WhereIF(!string.IsNullOrEmpty(input.spbm), p => p.Spbm.Contains(input.spbm)) 100 .WhereIF(!string.IsNullOrEmpty(input.spbm), p => p.Spbm.Contains(input.spbm))
92 .WhereIF(!string.IsNullOrEmpty(input.spxlhType), p => p.SpxlhType.Equals(input.spxlhType)) 101 .WhereIF(!string.IsNullOrEmpty(input.spxlhType), p => p.SpxlhType.Equals(input.spxlhType))
@@ -102,9 +111,7 @@ namespace NCC.Extend.WtSp @@ -102,9 +111,7 @@ namespace NCC.Extend.WtSp
102 { 111 {
103 id = it.Id, 112 id = it.Id,
104 spmc=it.Spmc, 113 spmc=it.Spmc,
105 - //pl=it.Pl,  
106 - pl=SqlFunc.Subqueryable<WtPlEntity>().Where(u=>u.Id==it.Pl).Select(u=>u.Plmc),  
107 - // pp=it.Pp, 114 + pl=it.Pl,
108 pp=SqlFunc.Subqueryable<WtPpEntity>().Where(u=>u.Id==it.Pp).Select(u=>u.Ppmc), 115 pp=SqlFunc.Subqueryable<WtPpEntity>().Where(u=>u.Id==it.Pp).Select(u=>u.Ppmc),
109 spbm=it.Spbm, 116 spbm=it.Spbm,
110 spxlhType=it.SpxlhType, 117 spxlhType=it.SpxlhType,
@@ -159,6 +166,7 @@ namespace NCC.Extend.WtSp @@ -159,6 +166,7 @@ namespace NCC.Extend.WtSp
159 { 166 {
160 item.mdkc = stockDict.ContainsKey(item.id) ? stockDict[item.id].ToString() : "0"; 167 item.mdkc = stockDict.ContainsKey(item.id) ? stockDict[item.id].ToString() : "0";
161 } 168 }
  169 + await ResolvePlDisplayAsync(resultList);
162 await ResolveHyxzDisplayAsync(resultList); 170 await ResolveHyxzDisplayAsync(resultList);
163 } 171 }
164 } 172 }
@@ -187,18 +195,13 @@ namespace NCC.Extend.WtSp @@ -187,18 +195,13 @@ namespace NCC.Extend.WtSp
187 var sidx = input.sidx == null ? "id" : input.sidx; 195 var sidx = input.sidx == null ? "id" : input.sidx;
188 196
189 197
190 - var data = await _db.Queryable<WtSpEntity>()  
191 - .WhereIF(!string.IsNullOrEmpty(input.keyword), p => p.Spmc.Contains(input.keyword)||p.Spbm.Contains(input.keyword)||p.Dyspid.Contains(input.keyword))  
192 - .WhereIF(!string.IsNullOrEmpty(input.pl), p => p.Pl.Equals(input.pl))  
193 -  
194 - 198 + var data = await WherePlCategoryIfAny(_db.Queryable<WtSpEntity>()
  199 + .WhereIF(!string.IsNullOrEmpty(input.keyword), p => p.Spmc.Contains(input.keyword)||p.Spbm.Contains(input.keyword)||p.Dyspid.Contains(input.keyword)), input.pl)
195 .Select(it=> new WtSpListOutput 200 .Select(it=> new WtSpListOutput
196 { 201 {
197 id = it.Id, 202 id = it.Id,
198 spmc=it.Spmc, 203 spmc=it.Spmc,
199 - //pl=it.Pl,  
200 - pl=SqlFunc.Subqueryable<WtPlEntity>().Where(u=>u.Id==it.Pl).Select(u=>u.Plmc),  
201 - // pp=it.Pp, 204 + pl=it.Pl,
202 pp=SqlFunc.Subqueryable<WtPpEntity>().Where(u=>u.Id==it.Pp).Select(u=>u.Ppmc), 205 pp=SqlFunc.Subqueryable<WtPpEntity>().Where(u=>u.Id==it.Pp).Select(u=>u.Ppmc),
203 spbm=it.Spbm, 206 spbm=it.Spbm,
204 spxlhType=it.SpxlhType, 207 spxlhType=it.SpxlhType,
@@ -255,6 +258,7 @@ namespace NCC.Extend.WtSp @@ -255,6 +258,7 @@ namespace NCC.Extend.WtSp
255 { 258 {
256 item.mdkc = stockDict.ContainsKey(item.id) ? stockDict[item.id].ToString() : "0"; 259 item.mdkc = stockDict.ContainsKey(item.id) ? stockDict[item.id].ToString() : "0";
257 } 260 }
  261 + await ResolvePlDisplayAsync(resultList);
258 await ResolveHyxzDisplayAsync(resultList); 262 await ResolveHyxzDisplayAsync(resultList);
259 } 263 }
260 } 264 }
@@ -267,6 +271,44 @@ namespace NCC.Extend.WtSp @@ -267,6 +271,44 @@ namespace NCC.Extend.WtSp
267 return pageResult; 271 return pageResult;
268 } 272 }
269 273
  274 + /// <summary>
  275 + /// 规范化商品品类:去空格、去重、去空段。
  276 + /// </summary>
  277 + private static string NormalizePlCsv(string pl)
  278 + {
  279 + if (string.IsNullOrWhiteSpace(pl)) return pl;
  280 + var parts = pl.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
  281 + .Where(s => !string.IsNullOrEmpty(s))
  282 + .Distinct(StringComparer.Ordinal)
  283 + .ToList();
  284 + return parts.Count == 0 ? null : string.Join(",", parts);
  285 + }
  286 +
  287 + /// <summary>
  288 + /// 商品品类 F_Pl 支持逗号分隔多个品类 ID;列表接口将 pl 字段解析为「品类名、品类名」展示。
  289 + /// </summary>
  290 + private async Task ResolvePlDisplayAsync(List<WtSpListOutput> list)
  291 + {
  292 + var allIds = list
  293 + .Where(p => !string.IsNullOrWhiteSpace(p.pl))
  294 + .SelectMany(p => p.pl.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
  295 + .Distinct()
  296 + .ToList();
  297 + if (allIds.Count == 0) return;
  298 + var plRows = await _db.Queryable<WtPlEntity>()
  299 + .Where(u => allIds.Contains(u.Id))
  300 + .Select(u => new { u.Id, u.Plmc })
  301 + .ToListAsync();
  302 + var dict = plRows.ToDictionary(x => x.Id, x => x.Plmc ?? "");
  303 + foreach (var item in list)
  304 + {
  305 + if (string.IsNullOrWhiteSpace(item.pl)) continue;
  306 + var ids = item.pl.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
  307 + var names = ids.Select(id => dict.TryGetValue(id, out var n) && !string.IsNullOrEmpty(n) ? n : id).ToList();
  308 + item.pl = names.Count > 0 ? string.Join("、", names) : item.pl;
  309 + }
  310 + }
  311 +
270 private async Task ResolveHyxzDisplayAsync(List<WtSpListOutput> list) 312 private async Task ResolveHyxzDisplayAsync(List<WtSpListOutput> list)
271 { 313 {
272 var ids = list.Where(p => !string.IsNullOrWhiteSpace(p.hyxz)) 314 var ids = list.Where(p => !string.IsNullOrWhiteSpace(p.hyxz))
@@ -341,6 +383,7 @@ namespace NCC.Extend.WtSp @@ -341,6 +383,7 @@ namespace NCC.Extend.WtSp
341 383
342 var entity = input.Adapt<WtSpEntity>(); 384 var entity = input.Adapt<WtSpEntity>();
343 entity.Id = YitIdHelper.NextId().ToString(); 385 entity.Id = YitIdHelper.NextId().ToString();
  386 + entity.Pl = NormalizePlCsv(entity.Pl);
344 387
345 // Console.WriteLine($"映射后的实体: {System.Text.Json.JsonSerializer.Serialize(entity)}"); 388 // Console.WriteLine($"映射后的实体: {System.Text.Json.JsonSerializer.Serialize(entity)}");
346 389
@@ -373,9 +416,8 @@ namespace NCC.Extend.WtSp @@ -373,9 +416,8 @@ namespace NCC.Extend.WtSp
373 List<object> queryKc = input.kc != null ? input.kc.Split(',').ToObeject<List<object>>() : null; 416 List<object> queryKc = input.kc != null ? input.kc.Split(',').ToObeject<List<object>>() : null;
374 var startKc = input.kc != null && !string.IsNullOrEmpty(queryKc.First().ToString()) ? queryKc.First() : decimal.MinValue; 417 var startKc = input.kc != null && !string.IsNullOrEmpty(queryKc.First().ToString()) ? queryKc.First() : decimal.MinValue;
375 var endKc = input.kc != null && !string.IsNullOrEmpty(queryKc.Last().ToString()) ? queryKc.Last() : decimal.MaxValue; 418 var endKc = input.kc != null && !string.IsNullOrEmpty(queryKc.Last().ToString()) ? queryKc.Last() : decimal.MaxValue;
376 - var data = await _db.Queryable<WtSpEntity>()  
377 - .WhereIF(!string.IsNullOrEmpty(input.spmc), p => p.Spmc.Contains(input.spmc))  
378 - .WhereIF(!string.IsNullOrEmpty(input.pl), p => p.Pl.Equals(input.pl)) 419 + var data = await WherePlCategoryIfAny(_db.Queryable<WtSpEntity>()
  420 + .WhereIF(!string.IsNullOrEmpty(input.spmc), p => p.Spmc.Contains(input.spmc)), input.pl)
379 .WhereIF(!string.IsNullOrEmpty(input.pp), p => p.Pp.Equals(input.pp)) 421 .WhereIF(!string.IsNullOrEmpty(input.pp), p => p.Pp.Equals(input.pp))
380 .WhereIF(!string.IsNullOrEmpty(input.spbm), p => p.Spbm.Contains(input.spbm)) 422 .WhereIF(!string.IsNullOrEmpty(input.spbm), p => p.Spbm.Contains(input.spbm))
381 .WhereIF(!string.IsNullOrEmpty(input.spxlhType), p => p.SpxlhType.Equals(input.spxlhType)) 423 .WhereIF(!string.IsNullOrEmpty(input.spxlhType), p => p.SpxlhType.Equals(input.spxlhType))
@@ -444,6 +486,9 @@ namespace NCC.Extend.WtSp @@ -444,6 +486,9 @@ namespace NCC.Extend.WtSp
444 item.mdkc = "0"; 486 item.mdkc = "0";
445 } 487 }
446 } 488 }
  489 +
  490 + await ResolvePlDisplayAsync(typedData);
  491 + await ResolveHyxzDisplayAsync(typedData);
447 } 492 }
448 493
449 return typedData; 494 return typedData;
@@ -571,6 +616,7 @@ namespace NCC.Extend.WtSp @@ -571,6 +616,7 @@ namespace NCC.Extend.WtSp
571 // Console.WriteLine($"接收到的输入数据: {System.Text.Json.JsonSerializer.Serialize(input)}"); 616 // Console.WriteLine($"接收到的输入数据: {System.Text.Json.JsonSerializer.Serialize(input)}");
572 617
573 var entity = input.Adapt<WtSpEntity>(); 618 var entity = input.Adapt<WtSpEntity>();
  619 + entity.Pl = NormalizePlCsv(entity.Pl);
574 620
575 // Console.WriteLine($"映射后的实体: {System.Text.Json.JsonSerializer.Serialize(entity)}"); 621 // Console.WriteLine($"映射后的实体: {System.Text.Json.JsonSerializer.Serialize(entity)}");
576 622