Commit 1946f6a5e9dcc601eb077f8fa2a923fbdf839179
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
Showing
15 changed files
with
511 additions
and
208 deletions
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
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[]) => { | @@ -152,7 +156,7 @@ export const getMergedOrderDetail = (ids: number[]) => { | ||
| 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 => { | @@ -48,3 +48,14 @@ export const getBackendBaseUrl = (): string => { | ||
| 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 'vue-router' | @@ -442,6 +455,7 @@ import { useRoute, useRouter } from 'vue-router' | ||
| 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 () => { | @@ -602,9 +627,10 @@ const loadOrderDetail = async () => { | ||
| 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 () => { | @@ -626,33 +652,7 @@ const loadOrderDetail = async () => { | ||
| 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) => { | @@ -733,7 +733,7 @@ const addProduct = async (product: any) => { | ||
| 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) => { | @@ -743,7 +743,7 @@ const addProduct = async (product: any) => { | ||
| 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) => { | @@ -804,32 +804,31 @@ const removeProduct = (index: number) => { | ||
| 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) => { | @@ -837,54 +836,52 @@ const getProductInfo = async (productCode: string) => { | ||
| 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) => { | @@ -943,11 +940,10 @@ const refreshSerialNumberType = async (row: any) => { | ||
| 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 () => { | @@ -1260,6 +1256,11 @@ const handleSubmit = async () => { | ||
| 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 () => { | @@ -363,7 +364,7 @@ const fetchOrders = async () => { | ||
| 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 () => { | @@ -623,10 +624,10 @@ const handleBatchCreateWaybill = async () => { | ||
| 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 () => { | @@ -719,6 +720,10 @@ const handleBatchCreateWaybill = async () => { | ||
| 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) => { | @@ -898,7 +903,8 @@ const getStatusText = (status: number) => { | ||
| 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) => { | @@ -909,7 +915,8 @@ const getStatusType = (status: number) => { | ||
| 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
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 |