Commit d3d42d61809504d652ca2f4ec22b9c3a468c7996
1 parent
81aab8ac
```
feat(utils): 支持退款模式的收付款显示格式化 - 添加可选参数 `opts` 配置对象,支持 `mode`, `skLabel`, `fkLabel`, `fallbackText` 选项 - 新增 refund 模式,收付款段前缀分别显示「退款」「退回」 - 兜底文案从「组合支付」改为「组合退款」 - 优化参数处理逻辑 fix(todoCenter): 商品调价单审核提示更新 - 更新商品调价单审核提示信息,明确说明审核后调后成本将写入 该仓库商品成本(wt_sp_cost.cbj) feat(wtCgthd): 序列号展示优化 - 序列号列宽度调整至 200px - 实现序列号预览功能,超出 8 个则仅显示前 4 个 + 查看按钮 - 添加序列号列表弹窗,展示完整序列号清单 - 新增相关计算方法和对话框组件 feat(wtDyDpsz): 抖音店铺配置功能增强 - 扩展表单布局至 820px 宽度 - 增加启用状态、同步天数等字段 - 添加抖音开放平台配置区域(AppKey/AppSecret) - 新增发货人信息配置(姓名、电话、地址等) - 表格视图增加出库仓库、AppKey、同步天数等列 - 实现出库仓库数据加载和展示 fix(forms): 商品选择清空处理 - 在多个表单组件中添加商品编号为空时的数据清空逻辑 - 清空商品名称、单位、数量、单价、金额等相关字段 - 重置序列号加载状态和列表数据 docs(wtPriceAdjust): 商品调价单文档更新 - 将调价单功能从调后售价改为调后成本 - 更新表格列标题:「调前售价」→「调前成本」 - 更新输入框占位符:「价格」→「新成本」 - 修正审核提示文本,说明调后成本写入仓库商品成本 ```
Showing
58 changed files
with
2935 additions
and
772 deletions
Antis.Erp.Plat/antis-ncc-admin/src/utils/wtComboSkzhDisplay.js
| ... | ... | @@ -59,11 +59,22 @@ export function parseWtMxJsonArray(val) { |
| 59 | 59 | * |
| 60 | 60 | * @param {Object} row - { skzh, skmx } |
| 61 | 61 | * @param {Array} skzhOptions - 与 dynamicText 一致的字典选项 |
| 62 | + * @param {Object} [opts] - 可选配置 | |
| 63 | + * - mode: 'sale' | 'refund',默认 'sale';refund 模式下 | |
| 64 | + * skmx 段前缀显示「退款」,fkmx 段前缀显示「退回」, | |
| 65 | + * 并把 `组合支付` 文案兜底为「组合退款」 | |
| 66 | + * - skLabel / fkLabel: 显式指定段前缀,优先级高于 mode | |
| 67 | + * - fallbackText: 无法解析时兜底文字 | |
| 62 | 68 | * @returns {string} |
| 63 | 69 | */ |
| 64 | -export function formatWtSkzhDisplay(row, skzhOptions) { | |
| 70 | +export function formatWtSkzhDisplay(row, skzhOptions, opts) { | |
| 65 | 71 | if (!row || row.skzh === null || row.skzh === undefined || row.skzh === '') return '无' |
| 66 | 72 | const options = skzhOptions || [] |
| 73 | + const o = opts || {} | |
| 74 | + const mode = o.mode === 'refund' ? 'refund' : 'sale' | |
| 75 | + const skLabel = o.skLabel || (mode === 'refund' ? '退款' : '收款') | |
| 76 | + const fkLabel = o.fkLabel || (mode === 'refund' ? '退回' : '付款') | |
| 77 | + const fallback = o.fallbackText || (mode === 'refund' ? '组合退款' : '组合支付') | |
| 67 | 78 | if (row.skzh === '组合支付' && (row.skmx || row.fkmx)) { |
| 68 | 79 | try { |
| 69 | 80 | const { list: skList } = row.skmx ? parseWtMxJsonArray(row.skmx) : { list: [] } |
| ... | ... | @@ -98,11 +109,11 @@ export function formatWtSkzhDisplay(row, skzhOptions) { |
| 98 | 109 | } |
| 99 | 110 | } |
| 100 | 111 | const seg = [] |
| 101 | - if (skParts.length) seg.push(`收款:${skParts.join(';')}`) | |
| 102 | - if (fkParts.length) seg.push(`付款:${fkParts.join(';')}`) | |
| 103 | - return seg.join('|') || '组合支付' | |
| 112 | + if (skParts.length) seg.push(`${skLabel}:${skParts.join(';')}`) | |
| 113 | + if (fkParts.length) seg.push(`${fkLabel}:${fkParts.join(';')}`) | |
| 114 | + return seg.join('|') || fallback | |
| 104 | 115 | } catch (e) { |
| 105 | - return '组合支付' | |
| 116 | + return fallback | |
| 106 | 117 | } |
| 107 | 118 | } |
| 108 | 119 | const byDict = resolveSkzhDictionaryLabel(row.skzh, options) | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/basic/todoCenter/index.vue
| ... | ... | @@ -654,7 +654,7 @@ export default { |
| 654 | 654 | } |
| 655 | 655 | if (t === "商品调价单") { |
| 656 | 656 | return { |
| 657 | - msg: "确认审核该商品调价单?通过后调后售价将写入商品档案零售价。", | |
| 657 | + msg: "确认审核该商品调价单?通过后调后成本将写入该仓库商品成本(wt_sp_cost.cbj)。", | |
| 658 | 658 | title: "审核确认" |
| 659 | 659 | }; |
| 660 | 660 | } |
| ... | ... | @@ -697,7 +697,7 @@ export default { |
| 697 | 697 | } |
| 698 | 698 | if (t === "商品调价单") { |
| 699 | 699 | return { |
| 700 | - msg: "确认二级审核该商品调价单?通过后调后售价将写入商品档案零售价。", | |
| 700 | + msg: "确认二级审核该商品调价单?通过后调后成本将写入该仓库商品成本(wt_sp_cost.cbj)。", | |
| 701 | 701 | title: "二级审核确认" |
| 702 | 702 | }; |
| 703 | 703 | } | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtBjdbd/Form.vue
| ... | ... | @@ -1639,6 +1639,18 @@ |
| 1639 | 1639 | return label.includes(query.toLowerCase()); |
| 1640 | 1640 | }, |
| 1641 | 1641 | async handleProductChange(row) { |
| 1642 | + if (!row.spbh) { | |
| 1643 | + this.$set(row, 'spmc', '') | |
| 1644 | + this.$set(row, 'dw', '') | |
| 1645 | + this.$set(row, 'sl', undefined) | |
| 1646 | + this.$set(row, 'dj', undefined) | |
| 1647 | + this.$set(row, 'je', undefined) | |
| 1648 | + this.$set(row, 'description', '') | |
| 1649 | + this.$set(row, 'kucun', undefined) | |
| 1650 | + this.$set(row, 'spxlhLoaded', false) | |
| 1651 | + this.$set(row, 'xlhList', []) | |
| 1652 | + return | |
| 1653 | + } | |
| 1642 | 1654 | // 选中商品后可自动回填商品名称等信息 |
| 1643 | 1655 | const product = this.spbhOptions.find(item => item.F_Id === row.spbh); |
| 1644 | 1656 | if (product) { | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtBsd/Form.vue
| ... | ... | @@ -1422,6 +1422,18 @@ |
| 1422 | 1422 | return label.includes(query.toLowerCase()); |
| 1423 | 1423 | }, |
| 1424 | 1424 | handleProductChange(row) { |
| 1425 | + if (!row.spbh) { | |
| 1426 | + this.$set(row, 'spmc', '') | |
| 1427 | + this.$set(row, 'dw', '') | |
| 1428 | + this.$set(row, 'sl', undefined) | |
| 1429 | + this.$set(row, 'dj', undefined) | |
| 1430 | + this.$set(row, 'je', undefined) | |
| 1431 | + this.$set(row, 'description', '') | |
| 1432 | + this.$set(row, 'kucun', undefined) | |
| 1433 | + this.$set(row, 'spxlhLoaded', false) | |
| 1434 | + this.$set(row, 'xlhList', []) | |
| 1435 | + return | |
| 1436 | + } | |
| 1425 | 1437 | // 选中商品后可自动回填商品名称等信息 |
| 1426 | 1438 | const product = this.spbhOptions.find(item => item.F_Id === row.spbh); |
| 1427 | 1439 | if (product) { | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtByd/Form.vue
| ... | ... | @@ -1422,6 +1422,18 @@ |
| 1422 | 1422 | return label.includes(query.toLowerCase()); |
| 1423 | 1423 | }, |
| 1424 | 1424 | handleProductChange(row) { |
| 1425 | + if (!row.spbh) { | |
| 1426 | + this.$set(row, 'spmc', '') | |
| 1427 | + this.$set(row, 'dw', '') | |
| 1428 | + this.$set(row, 'sl', undefined) | |
| 1429 | + this.$set(row, 'dj', undefined) | |
| 1430 | + this.$set(row, 'je', undefined) | |
| 1431 | + this.$set(row, 'description', '') | |
| 1432 | + this.$set(row, 'kucun', undefined) | |
| 1433 | + this.$set(row, 'spxlhLoaded', false) | |
| 1434 | + this.$set(row, 'xlhList', []) | |
| 1435 | + return | |
| 1436 | + } | |
| 1425 | 1437 | // 选中商品后可自动回填商品名称等信息 |
| 1426 | 1438 | const product = this.spbhOptions.find(item => item.F_Id === row.spbh); |
| 1427 | 1439 | if (product) { | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtCgrkd/Form.vue
| ... | ... | @@ -343,6 +343,14 @@ setFullName(item,row){ |
| 343 | 343 | } |
| 344 | 344 | }, |
| 345 | 345 | handleProductChange(row) { |
| 346 | + if (!row.spbh) { | |
| 347 | + this.$set(row, 'spmc', '') | |
| 348 | + this.$set(row, 'dw', '') | |
| 349 | + this.$set(row, 'sl', undefined) | |
| 350 | + this.$set(row, 'dj', undefined) | |
| 351 | + this.$set(row, 'je', undefined) | |
| 352 | + return | |
| 353 | + } | |
| 346 | 354 | const product = this.spbhOptions.find(item => item.F_Id === row.spbh); |
| 347 | 355 | if (product) { |
| 348 | 356 | row.spmc = product.F_Spmc || ''; | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtCgthd/Form.vue
| ... | ... | @@ -648,6 +648,20 @@ |
| 648 | 648 | this.updateTotalAmount(); |
| 649 | 649 | }, |
| 650 | 650 | async handleProductChange(row) { |
| 651 | + if (!row.spbh) { | |
| 652 | + this.$set(row, 'spmc', '') | |
| 653 | + this.$set(row, 'sptm', '') | |
| 654 | + this.$set(row, 'dw', '') | |
| 655 | + this.$set(row, 'sl', undefined) | |
| 656 | + this.$set(row, 'dj', '') | |
| 657 | + this.$set(row, 'thdj', '') | |
| 658 | + this.$set(row, 'je', '') | |
| 659 | + this.$set(row, 'description', '') | |
| 660 | + this.$set(row, 'dyddbh', undefined) | |
| 661 | + this.$set(row, 'spxlhLoaded', false) | |
| 662 | + this.$set(row, 'xlhList', []) | |
| 663 | + return | |
| 664 | + } | |
| 651 | 665 | const product = this.spbhOptions.find(item => item.F_Id === row.spbh); |
| 652 | 666 | if (product) { |
| 653 | 667 | row.spmc = product.F_Spmc || ''; | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtCgthd/detail-view.vue
| ... | ... | @@ -116,17 +116,27 @@ |
| 116 | 116 | <span class="cell-nowrap mx-cell__money">{{ formatMoneyCol(scope.row.je) }}</span> |
| 117 | 117 | </template> |
| 118 | 118 | </el-table-column> |
| 119 | - <el-table-column label="序列号" min-width="180"> | |
| 119 | + <el-table-column label="序列号" min-width="200"> | |
| 120 | 120 | <template slot-scope="scope"> |
| 121 | - <div v-if="scope.row.selectedSerialNumbers && scope.row.selectedSerialNumbers.length" class="sn-tags"> | |
| 122 | - <el-tag | |
| 123 | - v-for="(sn, idx) in scope.row.selectedSerialNumbers" | |
| 124 | - :key="idx" | |
| 125 | - size="mini" | |
| 126 | - type="warning" | |
| 127 | - class="sn-tag" | |
| 128 | - >{{ sn }}</el-tag> | |
| 129 | - </div> | |
| 121 | + <template v-if="serialList(scope.row).length"> | |
| 122 | + <div class="sn-tags"> | |
| 123 | + <el-tag | |
| 124 | + v-for="(sn, idx) in serialPreview(scope.row)" | |
| 125 | + :key="idx" | |
| 126 | + size="mini" | |
| 127 | + type="warning" | |
| 128 | + class="sn-tag" | |
| 129 | + >{{ sn }}</el-tag> | |
| 130 | + <el-button | |
| 131 | + v-if="serialList(scope.row).length > snInlineMax" | |
| 132 | + type="text" | |
| 133 | + class="sn-more-btn" | |
| 134 | + @click="openSerialDialog(scope.row)" | |
| 135 | + > | |
| 136 | + 共 {{ serialList(scope.row).length }} 个,点击查看 | |
| 137 | + </el-button> | |
| 138 | + </div> | |
| 139 | + </template> | |
| 130 | 140 | <span v-else class="text-muted">无</span> |
| 131 | 141 | </template> |
| 132 | 142 | </el-table-column> |
| ... | ... | @@ -179,6 +189,26 @@ |
| 179 | 189 | <span>暂无数据</span> |
| 180 | 190 | </div> |
| 181 | 191 | </div> |
| 192 | + | |
| 193 | + <el-dialog | |
| 194 | + title="序列号列表" | |
| 195 | + :visible.sync="serialDialogVisible" | |
| 196 | + width="560px" | |
| 197 | + append-to-body | |
| 198 | + class="NCC-dialog cgthd-sn-list-dialog" | |
| 199 | + @closed="serialDialogList = []" | |
| 200 | + > | |
| 201 | + <div class="sn-dialog-hint">共 {{ serialDialogList.length }} 条</div> | |
| 202 | + <div class="sn-dialog-tags"> | |
| 203 | + <el-tag | |
| 204 | + v-for="(sn, idx) in serialDialogList" | |
| 205 | + :key="idx" | |
| 206 | + size="small" | |
| 207 | + type="warning" | |
| 208 | + class="sn-dialog-tag" | |
| 209 | + >{{ sn }}</el-tag> | |
| 210 | + </div> | |
| 211 | + </el-dialog> | |
| 182 | 212 | </el-dialog> |
| 183 | 213 | </template> |
| 184 | 214 | |
| ... | ... | @@ -196,7 +226,12 @@ export default { |
| 196 | 226 | loading: false, |
| 197 | 227 | detail: null, |
| 198 | 228 | ckckOptions: [], |
| 199 | - skzhOptions: [] | |
| 229 | + skzhOptions: [], | |
| 230 | + /** 明细格内最多直接展示的序列号个数,超出则仅预览前若干 + 点击查看 */ | |
| 231 | + snInlineMax: 8, | |
| 232 | + snPreviewCount: 4, | |
| 233 | + serialDialogVisible: false, | |
| 234 | + serialDialogList: [] | |
| 200 | 235 | } |
| 201 | 236 | }, |
| 202 | 237 | computed: { |
| ... | ... | @@ -215,6 +250,20 @@ export default { |
| 215 | 250 | } |
| 216 | 251 | }, |
| 217 | 252 | methods: { |
| 253 | + serialList(row) { | |
| 254 | + const raw = row && row.selectedSerialNumbers | |
| 255 | + if (!raw || !raw.length) return [] | |
| 256 | + return raw.map(s => (s == null ? '' : String(s)).trim()).filter(Boolean) | |
| 257 | + }, | |
| 258 | + serialPreview(row) { | |
| 259 | + const all = this.serialList(row) | |
| 260 | + if (all.length <= this.snInlineMax) return all | |
| 261 | + return all.slice(0, this.snPreviewCount) | |
| 262 | + }, | |
| 263 | + openSerialDialog(row) { | |
| 264 | + this.serialDialogList = this.serialList(row).slice() | |
| 265 | + this.serialDialogVisible = true | |
| 266 | + }, | |
| 218 | 267 | labelFromOptions(value, options) { |
| 219 | 268 | if (value === null || value === undefined || value === '') return '无' |
| 220 | 269 | const opts = options || [] |
| ... | ... | @@ -261,6 +310,8 @@ export default { |
| 261 | 310 | return sums |
| 262 | 311 | }, |
| 263 | 312 | handleClose() { |
| 313 | + this.serialDialogVisible = false | |
| 314 | + this.serialDialogList = [] | |
| 264 | 315 | this.detail = null |
| 265 | 316 | this.$emit('close') |
| 266 | 317 | }, |
| ... | ... | @@ -450,6 +501,29 @@ export default { |
| 450 | 501 | .sn-tag { |
| 451 | 502 | margin: 0 !important; |
| 452 | 503 | } |
| 504 | +.sn-more-btn { | |
| 505 | + padding: 0 4px !important; | |
| 506 | + margin-left: 2px; | |
| 507 | + vertical-align: middle; | |
| 508 | + font-size: 12px; | |
| 509 | +} | |
| 510 | +.sn-dialog-hint { | |
| 511 | + font-size: 12px; | |
| 512 | + color: #909399; | |
| 513 | + margin: -6px 0 10px; | |
| 514 | +} | |
| 515 | +.sn-dialog-tags { | |
| 516 | + max-height: min(420px, 60vh); | |
| 517 | + overflow-y: auto; | |
| 518 | + display: flex; | |
| 519 | + flex-wrap: wrap; | |
| 520 | + gap: 6px; | |
| 521 | + align-content: flex-start; | |
| 522 | + padding: 2px 0; | |
| 523 | +} | |
| 524 | +.sn-dialog-tag { | |
| 525 | + margin: 0 !important; | |
| 526 | +} | |
| 453 | 527 | .detail-bz { |
| 454 | 528 | white-space: pre-wrap; |
| 455 | 529 | word-break: break-all; | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtCzd/Form.vue
Antis.Erp.Plat/antis-ncc-admin/src/views/wtDyDpsz/Form.vue
| 1 | 1 | <template> |
| 2 | - <el-dialog :title="!dataForm.id ? '新建' : '编辑'" :close-on-click-modal="false" :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="600px"> | |
| 2 | + <el-dialog :title="!dataForm.id ? '新建' : '编辑'" :close-on-click-modal="false" :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="820px"> | |
| 3 | 3 | <el-row :gutter="15"> |
| 4 | - <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :rules="rules"> | |
| 4 | + <el-form ref="elForm" :model="dataForm" size="small" label-width="95px" label-position="right" :rules="rules"> | |
| 5 | 5 | <el-col :span="24"> |
| 6 | + <el-divider content-position="left">基础信息</el-divider> | |
| 7 | + </el-col> | |
| 8 | + <el-col :span="12"> | |
| 6 | 9 | <el-form-item label="店铺ID" prop="shopId"> |
| 7 | 10 | <el-input-number v-model="dataForm.shopId" :min="0" :precision="0" placeholder="抖音店铺ID" controls-position="right" style="width:100%" /> |
| 8 | 11 | </el-form-item> |
| 9 | 12 | </el-col> |
| 10 | - <el-col :span="24"> | |
| 13 | + <el-col :span="12"> | |
| 11 | 14 | <el-form-item label="店铺名称" prop="shopName"> |
| 12 | 15 | <el-input v-model="dataForm.shopName" placeholder="店铺名称" clearable style="width:100%" /> |
| 13 | 16 | </el-form-item> |
| 14 | 17 | </el-col> |
| 18 | + <el-col :span="12"> | |
| 19 | + <el-form-item label="启用状态" prop="enabled"> | |
| 20 | + <el-select v-model="dataForm.enabled" placeholder="启用状态" style="width:100%"> | |
| 21 | + <el-option label="启用" :value="1" /> | |
| 22 | + <el-option label="停用" :value="0" /> | |
| 23 | + </el-select> | |
| 24 | + </el-form-item> | |
| 25 | + </el-col> | |
| 26 | + <el-col :span="12"> | |
| 27 | + <el-form-item label="同步天数" prop="syncDays"> | |
| 28 | + <el-input-number v-model="dataForm.syncDays" :min="1" :max="365" :precision="0" controls-position="right" style="width:100%" /> | |
| 29 | + </el-form-item> | |
| 30 | + </el-col> | |
| 31 | + | |
| 32 | + <el-col :span="24"> | |
| 33 | + <el-divider content-position="left">抖音开放平台配置</el-divider> | |
| 34 | + </el-col> | |
| 35 | + <el-col :span="24"> | |
| 36 | + <el-form-item label="AppKey" prop="appKey"> | |
| 37 | + <el-input v-model="dataForm.appKey" placeholder="抖音开放平台 AppKey" clearable style="width:100%" /> | |
| 38 | + </el-form-item> | |
| 39 | + </el-col> | |
| 15 | 40 | <el-col :span="24"> |
| 41 | + <el-form-item label="AppSecret" prop="appSecret"> | |
| 42 | + <el-input v-model="dataForm.appSecret" placeholder="抖音开放平台 AppSecret" show-password clearable style="width:100%" /> | |
| 43 | + </el-form-item> | |
| 44 | + </el-col> | |
| 45 | + <el-col :span="24"> | |
| 46 | + <el-form-item label="回调地址" prop="callbackUrl"> | |
| 47 | + <el-input v-model="dataForm.callbackUrl" placeholder="授权回调地址,例如 http://xxx/api/auth/callback" clearable style="width:100%" /> | |
| 48 | + </el-form-item> | |
| 49 | + </el-col> | |
| 50 | + <el-col :span="24"> | |
| 51 | + <el-form-item label="API 地址" prop="apiBaseUrl"> | |
| 52 | + <el-input v-model="dataForm.apiBaseUrl" placeholder="默认 https://openapi-fxg.jinritemai.com" clearable style="width:100%" /> | |
| 53 | + </el-form-item> | |
| 54 | + </el-col> | |
| 55 | + | |
| 56 | + <el-col :span="24"> | |
| 57 | + <el-divider content-position="left">ERP 关联</el-divider> | |
| 58 | + </el-col> | |
| 59 | + <el-col :span="12"> | |
| 16 | 60 | <el-form-item label="往来单位" prop="kh"> |
| 17 | 61 | <el-select v-model="dataForm.kh" placeholder="请选择往来单位" clearable filterable style="width:100%"> |
| 18 | 62 | <el-option v-for="item in khList" :key="item.id" :label="item.dwmc" :value="item.id" /> |
| 19 | 63 | </el-select> |
| 20 | 64 | </el-form-item> |
| 21 | 65 | </el-col> |
| 22 | - <el-col :span="24"> | |
| 66 | + <el-col :span="12"> | |
| 23 | 67 | <el-form-item label="收款账户" prop="skzh"> |
| 24 | 68 | <el-select v-model="dataForm.skzh" placeholder="请选择收款账户" clearable style="width:100%"> |
| 25 | 69 | <el-option v-for="item in skzhList" :key="item.id" :label="item.fullName" :value="item.id" /> |
| ... | ... | @@ -27,6 +71,56 @@ |
| 27 | 71 | </el-form-item> |
| 28 | 72 | </el-col> |
| 29 | 73 | <el-col :span="24"> |
| 74 | + <el-form-item label="出库仓库" prop="ck"> | |
| 75 | + <el-select v-model="dataForm.ck" placeholder="请选择出库仓库(抖音发货单专用)" clearable filterable style="width:100%"> | |
| 76 | + <el-option v-for="item in ckList" :key="item.F_Id || item.id" :label="item.F_mdmc || item.mdmc" :value="item.F_Id || item.id" /> | |
| 77 | + </el-select> | |
| 78 | + </el-form-item> | |
| 79 | + </el-col> | |
| 80 | + | |
| 81 | + <el-col :span="24"> | |
| 82 | + <el-divider content-position="left">发货人信息</el-divider> | |
| 83 | + </el-col> | |
| 84 | + <el-col :span="12"> | |
| 85 | + <el-form-item label="发货人姓名" prop="senderName"> | |
| 86 | + <el-input v-model="dataForm.senderName" placeholder="发货人姓名" clearable style="width:100%" /> | |
| 87 | + </el-form-item> | |
| 88 | + </el-col> | |
| 89 | + <el-col :span="12"> | |
| 90 | + <el-form-item label="发货人电话" prop="senderPhone"> | |
| 91 | + <el-input v-model="dataForm.senderPhone" placeholder="发货人电话" clearable style="width:100%" /> | |
| 92 | + </el-form-item> | |
| 93 | + </el-col> | |
| 94 | + <el-col :span="8"> | |
| 95 | + <el-form-item label="省份" prop="senderProvince"> | |
| 96 | + <el-input v-model="dataForm.senderProvince" placeholder="省份" clearable style="width:100%" /> | |
| 97 | + </el-form-item> | |
| 98 | + </el-col> | |
| 99 | + <el-col :span="8"> | |
| 100 | + <el-form-item label="城市" prop="senderCity"> | |
| 101 | + <el-input v-model="dataForm.senderCity" placeholder="城市" clearable style="width:100%" /> | |
| 102 | + </el-form-item> | |
| 103 | + </el-col> | |
| 104 | + <el-col :span="8"> | |
| 105 | + <el-form-item label="区县" prop="senderDistrict"> | |
| 106 | + <el-input v-model="dataForm.senderDistrict" placeholder="区县" clearable style="width:100%" /> | |
| 107 | + </el-form-item> | |
| 108 | + </el-col> | |
| 109 | + <el-col :span="8"> | |
| 110 | + <el-form-item label="街道" prop="senderStreet"> | |
| 111 | + <el-input v-model="dataForm.senderStreet" placeholder="街道(可选)" clearable style="width:100%" /> | |
| 112 | + </el-form-item> | |
| 113 | + </el-col> | |
| 114 | + <el-col :span="16"> | |
| 115 | + <el-form-item label="详细地址" prop="senderAddress"> | |
| 116 | + <el-input v-model="dataForm.senderAddress" placeholder="详细地址(街道门牌等)" clearable style="width:100%" /> | |
| 117 | + </el-form-item> | |
| 118 | + </el-col> | |
| 119 | + | |
| 120 | + <el-col :span="24"> | |
| 121 | + <el-divider content-position="left">备注</el-divider> | |
| 122 | + </el-col> | |
| 123 | + <el-col :span="24"> | |
| 30 | 124 | <el-form-item label="备注" prop="bz"> |
| 31 | 125 | <el-input v-model="dataForm.bz" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" placeholder="可选" clearable style="width:100%" /> |
| 32 | 126 | </el-form-item> |
| ... | ... | @@ -41,30 +135,50 @@ |
| 41 | 135 | </template> |
| 42 | 136 | <script> |
| 43 | 137 | import request from '@/utils/request' |
| 138 | + | |
| 139 | +const emptyForm = () => ({ | |
| 140 | + id: undefined, | |
| 141 | + shopId: undefined, | |
| 142 | + shopName: undefined, | |
| 143 | + kh: undefined, | |
| 144 | + skzh: undefined, | |
| 145 | + ck: undefined, | |
| 146 | + bz: undefined, | |
| 147 | + appKey: undefined, | |
| 148 | + appSecret: undefined, | |
| 149 | + callbackUrl: undefined, | |
| 150 | + apiBaseUrl: 'https://openapi-fxg.jinritemai.com', | |
| 151 | + syncDays: 30, | |
| 152 | + senderName: undefined, | |
| 153 | + senderPhone: undefined, | |
| 154 | + senderAddress: undefined, | |
| 155 | + senderProvince: undefined, | |
| 156 | + senderCity: undefined, | |
| 157 | + senderDistrict: undefined, | |
| 158 | + senderStreet: undefined, | |
| 159 | + enabled: 1 | |
| 160 | +}) | |
| 161 | + | |
| 44 | 162 | export default { |
| 45 | 163 | props: { |
| 46 | 164 | khList: { type: Array, default: () => [] }, |
| 47 | - skzhList: { type: Array, default: () => [] } | |
| 165 | + skzhList: { type: Array, default: () => [] }, | |
| 166 | + ckList: { type: Array, default: () => [] } | |
| 48 | 167 | }, |
| 49 | 168 | data() { |
| 50 | 169 | return { |
| 51 | 170 | visible: false, |
| 52 | - dataForm: { | |
| 53 | - id: undefined, | |
| 54 | - shopId: undefined, | |
| 55 | - shopName: undefined, | |
| 56 | - kh: undefined, | |
| 57 | - skzh: undefined, | |
| 58 | - bz: undefined | |
| 59 | - }, | |
| 171 | + dataForm: emptyForm(), | |
| 60 | 172 | rules: { |
| 61 | 173 | shopId: [{ required: true, message: '请输入店铺ID', trigger: 'change' }], |
| 62 | - shopName: [{ required: true, message: '请输入店铺名称', trigger: 'blur' }] | |
| 174 | + shopName: [{ required: true, message: '请输入店铺名称', trigger: 'blur' }], | |
| 175 | + appKey: [{ required: true, message: '请输入 AppKey', trigger: 'blur' }] | |
| 63 | 176 | } |
| 64 | 177 | } |
| 65 | 178 | }, |
| 66 | 179 | methods: { |
| 67 | 180 | init(id) { |
| 181 | + this.dataForm = emptyForm() | |
| 68 | 182 | this.dataForm.id = id || 0 |
| 69 | 183 | this.visible = true |
| 70 | 184 | this.$nextTick(() => { |
| ... | ... | @@ -84,18 +198,23 @@ export default { |
| 84 | 198 | shopName: d.shopName, |
| 85 | 199 | kh: d.kh !== undefined && d.kh !== null && d.kh !== '' ? d.kh : undefined, |
| 86 | 200 | skzh: d.skzh !== undefined && d.skzh !== null && d.skzh !== '' ? d.skzh : undefined, |
| 87 | - bz: d.bz | |
| 201 | + ck: d.ck !== undefined && d.ck !== null && d.ck !== '' ? d.ck : undefined, | |
| 202 | + bz: d.bz, | |
| 203 | + appKey: d.appKey || '', | |
| 204 | + appSecret: d.appSecret || '', | |
| 205 | + callbackUrl: d.callbackUrl || '', | |
| 206 | + apiBaseUrl: d.apiBaseUrl || 'https://openapi-fxg.jinritemai.com', | |
| 207 | + syncDays: d.syncDays || 30, | |
| 208 | + senderName: d.senderName || '', | |
| 209 | + senderPhone: d.senderPhone || '', | |
| 210 | + senderAddress: d.senderAddress || '', | |
| 211 | + senderProvince: d.senderProvince || '', | |
| 212 | + senderCity: d.senderCity || '', | |
| 213 | + senderDistrict: d.senderDistrict || '', | |
| 214 | + senderStreet: d.senderStreet || '', | |
| 215 | + enabled: d.enabled === 0 ? 0 : 1 | |
| 88 | 216 | } |
| 89 | 217 | }) |
| 90 | - } else { | |
| 91 | - this.dataForm = { | |
| 92 | - id: undefined, | |
| 93 | - shopId: undefined, | |
| 94 | - shopName: undefined, | |
| 95 | - kh: undefined, | |
| 96 | - skzh: undefined, | |
| 97 | - bz: undefined | |
| 98 | - } | |
| 99 | 218 | } |
| 100 | 219 | }) |
| 101 | 220 | }, |
| ... | ... | @@ -107,7 +226,21 @@ export default { |
| 107 | 226 | shopName: this.dataForm.shopName, |
| 108 | 227 | kh: this.dataForm.kh, |
| 109 | 228 | skzh: this.dataForm.skzh, |
| 110 | - bz: this.dataForm.bz | |
| 229 | + ck: this.dataForm.ck, | |
| 230 | + bz: this.dataForm.bz, | |
| 231 | + appKey: this.dataForm.appKey, | |
| 232 | + appSecret: this.dataForm.appSecret, | |
| 233 | + callbackUrl: this.dataForm.callbackUrl, | |
| 234 | + apiBaseUrl: this.dataForm.apiBaseUrl, | |
| 235 | + syncDays: this.dataForm.syncDays, | |
| 236 | + senderName: this.dataForm.senderName, | |
| 237 | + senderPhone: this.dataForm.senderPhone, | |
| 238 | + senderAddress: this.dataForm.senderAddress, | |
| 239 | + senderProvince: this.dataForm.senderProvince, | |
| 240 | + senderCity: this.dataForm.senderCity, | |
| 241 | + senderDistrict: this.dataForm.senderDistrict, | |
| 242 | + senderStreet: this.dataForm.senderStreet, | |
| 243 | + enabled: this.dataForm.enabled | |
| 111 | 244 | } |
| 112 | 245 | if (!this.dataForm.id) { |
| 113 | 246 | request({ | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtDyDpsz/index.vue
| ... | ... | @@ -59,6 +59,35 @@ |
| 59 | 59 | {{ getSkzhName(scope.row.skzh) }} |
| 60 | 60 | </template> |
| 61 | 61 | </el-table-column> |
| 62 | + <el-table-column label="出库仓库" align="left" min-width="160" show-overflow-tooltip> | |
| 63 | + <template slot-scope="scope"> | |
| 64 | + {{ getCkName(scope.row.ck) }} | |
| 65 | + </template> | |
| 66 | + </el-table-column> | |
| 67 | + <el-table-column label="AppKey" align="left" min-width="180" show-overflow-tooltip> | |
| 68 | + <template slot-scope="scope"> | |
| 69 | + {{ scope.row.appKey || '未配置' }} | |
| 70 | + </template> | |
| 71 | + </el-table-column> | |
| 72 | + <el-table-column label="同步天数" align="center" width="90"> | |
| 73 | + <template slot-scope="scope"> | |
| 74 | + {{ scope.row.syncDays || 30 }} | |
| 75 | + </template> | |
| 76 | + </el-table-column> | |
| 77 | + <el-table-column label="发货人" align="left" min-width="160" show-overflow-tooltip> | |
| 78 | + <template slot-scope="scope"> | |
| 79 | + <span v-if="scope.row.senderName || scope.row.senderPhone"> | |
| 80 | + {{ scope.row.senderName || '' }}<span v-if="scope.row.senderPhone"> / {{ scope.row.senderPhone }}</span> | |
| 81 | + </span> | |
| 82 | + <span v-else>未配置</span> | |
| 83 | + </template> | |
| 84 | + </el-table-column> | |
| 85 | + <el-table-column label="状态" align="center" width="80"> | |
| 86 | + <template slot-scope="scope"> | |
| 87 | + <el-tag v-if="(scope.row.enabled === undefined || scope.row.enabled === null || scope.row.enabled === 1)" type="success" size="mini">启用</el-tag> | |
| 88 | + <el-tag v-else type="info" size="mini">停用</el-tag> | |
| 89 | + </template> | |
| 90 | + </el-table-column> | |
| 62 | 91 | <el-table-column prop="bz" label="备注" align="left" min-width="140" show-overflow-tooltip> |
| 63 | 92 | <template slot-scope="scope"> |
| 64 | 93 | {{ scope.row.bz || '无' }} |
| ... | ... | @@ -74,7 +103,7 @@ |
| 74 | 103 | </div> |
| 75 | 104 | </el-card> |
| 76 | 105 | </div> |
| 77 | - <NCC-Form v-if="formVisible" ref="NCCForm" :kh-list="khList" :skzh-list="skzhList" @refresh="refresh" /> | |
| 106 | + <NCC-Form v-if="formVisible" ref="NCCForm" :kh-list="khList" :skzh-list="skzhList" :ck-list="ckList" @refresh="refresh" /> | |
| 78 | 107 | </div> |
| 79 | 108 | </template> |
| 80 | 109 | <script> |
| ... | ... | @@ -91,7 +120,8 @@ export default { |
| 91 | 120 | listLoading: true, |
| 92 | 121 | formVisible: false, |
| 93 | 122 | khList: [], |
| 94 | - skzhList: [] | |
| 123 | + skzhList: [], | |
| 124 | + ckList: [] | |
| 95 | 125 | } |
| 96 | 126 | }, |
| 97 | 127 | computed: { |
| ... | ... | @@ -109,6 +139,7 @@ export default { |
| 109 | 139 | this.initData() |
| 110 | 140 | this.loadKhList() |
| 111 | 141 | this.loadSkzhList() |
| 142 | + this.loadCkList() | |
| 112 | 143 | }, |
| 113 | 144 | methods: { |
| 114 | 145 | initData() { |
| ... | ... | @@ -146,6 +177,28 @@ export default { |
| 146 | 177 | this.skzhList = [] |
| 147 | 178 | }) |
| 148 | 179 | }, |
| 180 | + loadCkList() { | |
| 181 | + request({ | |
| 182 | + url: '/api/Extend/WtCk', | |
| 183 | + method: 'GET', | |
| 184 | + data: { currentPage: 1, pageSize: 1000 } | |
| 185 | + }).then(res => { | |
| 186 | + const d = res.data | |
| 187 | + if (Array.isArray(d)) { | |
| 188 | + this.ckList = d | |
| 189 | + } else if (d && Array.isArray(d.list)) { | |
| 190 | + this.ckList = d.list | |
| 191 | + } else if (d && d.data && Array.isArray(d.data.list)) { | |
| 192 | + this.ckList = d.data.list | |
| 193 | + } else if (d && Array.isArray(d.data)) { | |
| 194 | + this.ckList = d.data | |
| 195 | + } else { | |
| 196 | + this.ckList = [] | |
| 197 | + } | |
| 198 | + }).catch(() => { | |
| 199 | + this.ckList = [] | |
| 200 | + }) | |
| 201 | + }, | |
| 149 | 202 | getKhName(id) { |
| 150 | 203 | if (id === undefined || id === null || id === '') return '无' |
| 151 | 204 | const item = this.khList.find(x => x.id === id || String(x.id) === String(id)) |
| ... | ... | @@ -156,6 +209,12 @@ export default { |
| 156 | 209 | const item = this.skzhList.find(x => x.id === id || String(x.id) === String(id)) |
| 157 | 210 | return item ? (item.fullName || '无') : '无' |
| 158 | 211 | }, |
| 212 | + getCkName(id) { | |
| 213 | + if (id === undefined || id === null || id === '') return '无' | |
| 214 | + const key = String(id) | |
| 215 | + const item = this.ckList.find(x => String(x.F_Id || x.id) === key) | |
| 216 | + return item ? (item.F_mdmc || item.mdmc || '无') : '无' | |
| 217 | + }, | |
| 159 | 218 | handleDel(id) { |
| 160 | 219 | this.$confirm('此操作将永久删除该店铺设置, 是否继续?', '提示', { |
| 161 | 220 | type: 'warning' | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtHzd/Form.vue
| ... | ... | @@ -1293,6 +1293,18 @@ |
| 1293 | 1293 | return label.includes(query.toLowerCase()); |
| 1294 | 1294 | }, |
| 1295 | 1295 | handleProductChange(row) { |
| 1296 | + if (!row.spbh) { | |
| 1297 | + this.$set(row, 'spmc', '') | |
| 1298 | + this.$set(row, 'dw', '') | |
| 1299 | + this.$set(row, 'sl', undefined) | |
| 1300 | + this.$set(row, 'dj', undefined) | |
| 1301 | + this.$set(row, 'je', undefined) | |
| 1302 | + this.$set(row, 'description', '') | |
| 1303 | + this.$set(row, 'kucun', undefined) | |
| 1304 | + this.$set(row, 'spxlhLoaded', false) | |
| 1305 | + this.$set(row, 'xlhList', []) | |
| 1306 | + return | |
| 1307 | + } | |
| 1296 | 1308 | // 选中商品后可自动回填商品名称等信息 |
| 1297 | 1309 | const product = this.spbhOptions.find(item => item.F_Id === row.spbh); |
| 1298 | 1310 | if (product) { | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtPdd/Form.vue
| ... | ... | @@ -1447,6 +1447,18 @@ |
| 1447 | 1447 | return label.includes(query.toLowerCase()); |
| 1448 | 1448 | }, |
| 1449 | 1449 | handleProductChange(row) { |
| 1450 | + if (!row.spbh) { | |
| 1451 | + this.$set(row, 'spmc', '') | |
| 1452 | + this.$set(row, 'dw', '') | |
| 1453 | + this.$set(row, 'sl', undefined) | |
| 1454 | + this.$set(row, 'dj', undefined) | |
| 1455 | + this.$set(row, 'je', undefined) | |
| 1456 | + this.$set(row, 'description', '') | |
| 1457 | + this.$set(row, 'kucun', undefined) | |
| 1458 | + this.$set(row, 'spxlhLoaded', false) | |
| 1459 | + this.$set(row, 'xlhList', []) | |
| 1460 | + return | |
| 1461 | + } | |
| 1450 | 1462 | // 选中商品后可自动回填商品名称等信息 |
| 1451 | 1463 | const product = this.spbhOptions.find(item => item.F_Id === row.spbh); |
| 1452 | 1464 | if (product) { | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtPriceAdjust/Form.vue
| ... | ... | @@ -110,12 +110,7 @@ |
| 110 | 110 | <span :class="{ 'text-warning': scope.row.sl === 0 }">{{ scope.row.sl }}</span> |
| 111 | 111 | </template> |
| 112 | 112 | </el-table-column> |
| 113 | - <el-table-column prop="cbj" label="成本(参考)" width="96" align="right"> | |
| 114 | - <template slot-scope="scope"> | |
| 115 | - <span class="tjd-ref-price">{{ formatPrice(scope.row.cbj) }}</span> | |
| 116 | - </template> | |
| 117 | - </el-table-column> | |
| 118 | - <el-table-column prop="tqdj" label="调前售价" width="90" align="right"> | |
| 113 | + <el-table-column prop="tqdj" label="调前成本" width="96" align="right"> | |
| 119 | 114 | <template slot-scope="scope"> |
| 120 | 115 | <span :class="{ 'text-warning': !(parseFloat(scope.row.tqdj) > 0) }">{{ formatPrice(scope.row.tqdj) }}</span> |
| 121 | 116 | </template> |
| ... | ... | @@ -127,15 +122,15 @@ |
| 127 | 122 | </el-input> |
| 128 | 123 | </template> |
| 129 | 124 | </el-table-column> |
| 130 | - <el-table-column prop="thdj" label="调后售价" width="100" class-name="tjd-editable-col"> | |
| 125 | + <el-table-column prop="thdj" label="调后成本" width="100" class-name="tjd-editable-col"> | |
| 131 | 126 | <template slot-scope="scope"> |
| 132 | - <el-input v-model="scope.row.thdj" size="mini" placeholder="价格" :disabled="!!isDetail" @input="handleNewPriceChange(scope.row)" /> | |
| 127 | + <el-input v-model="scope.row.thdj" size="mini" placeholder="新成本" :disabled="!!isDetail" @input="handleNewPriceChange(scope.row)" /> | |
| 133 | 128 | </template> |
| 134 | 129 | </el-table-column> |
| 135 | - <el-table-column prop="tqje" label="调前金额" width="90" align="right"> | |
| 130 | + <el-table-column prop="tqje" label="调前成本总额" width="110" align="right"> | |
| 136 | 131 | <template slot-scope="scope">{{ formatPrice(scope.row.tqje) }}</template> |
| 137 | 132 | </el-table-column> |
| 138 | - <el-table-column prop="thje" label="调后金额" width="90" align="right"> | |
| 133 | + <el-table-column prop="thje" label="调后成本总额" width="110" align="right"> | |
| 139 | 134 | <template slot-scope="scope"> |
| 140 | 135 | <span class="text-primary" v-if="scope.row.thje">{{ formatPrice(scope.row.thje) }}</span> |
| 141 | 136 | <span v-else>-</span> |
| ... | ... | @@ -316,7 +311,7 @@ |
| 316 | 311 | } |
| 317 | 312 | this.$set(row, 'ck', this.dataForm.ck || '') |
| 318 | 313 | }, |
| 319 | - // 调后售价 = 调前售价 × (比率% / 100),例如 100 为原价、110 为按 110% 计价 | |
| 314 | + // 调后成本 = 调前成本 × (比率% / 100),例如 100 为原价、110 为按 110% 计价 | |
| 320 | 315 | handleRatioChange(row) { |
| 321 | 316 | const tqdj = parseFloat(row.tqdj) || 0 |
| 322 | 317 | const tjbl = parseFloat(row.tjbl) |
| ... | ... | @@ -384,7 +379,7 @@ |
| 384 | 379 | }, |
| 385 | 380 | handleSubmitForAudit() { |
| 386 | 381 | if (!this.dataForm.id) return |
| 387 | - this.$confirm('确认提交审核?提交后将进入待办中心,由审核人员在待办中审批;通过后调后售价写入商品档案零售价。', '提交审核', { type: 'warning' }).then(() => { | |
| 382 | + this.$confirm('确认提交审核?提交后将进入待办中心,由审核人员在待办中审批;通过后调后成本写入该仓库商品成本(wt_sp_cost.cbj)。', '提交审核', { type: 'warning' }).then(() => { | |
| 388 | 383 | request({ url: `/api/Extend/WtTjd/Actions/SubmitForAudit/${this.dataForm.id}`, method: 'POST' }).then(res => { |
| 389 | 384 | this.$message({ type: 'success', message: res.msg || '已提交', duration: 1000, onClose: () => { this.visible = false; this.$emit('refresh', true) } }) |
| 390 | 385 | }) | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtPriceAdjust/detail-view.vue
| ... | ... | @@ -60,17 +60,17 @@ |
| 60 | 60 | </el-table-column> |
| 61 | 61 | <el-table-column prop="spbm" label="编码" width="100" show-overflow-tooltip /> |
| 62 | 62 | <el-table-column prop="sl" label="库存" width="72" align="right" /> |
| 63 | - <el-table-column label="调前售价" width="88" align="right"> | |
| 63 | + <el-table-column label="调前成本" width="92" align="right"> | |
| 64 | 64 | <template slot-scope="scope">{{ formatMoney(scope.row.tqdj) }}</template> |
| 65 | 65 | </el-table-column> |
| 66 | 66 | <el-table-column prop="tjbl" label="比率%" width="72" align="right" /> |
| 67 | - <el-table-column label="调后售价" width="88" align="right"> | |
| 67 | + <el-table-column label="调后成本" width="92" align="right"> | |
| 68 | 68 | <template slot-scope="scope">{{ formatMoney(scope.row.thdj) }}</template> |
| 69 | 69 | </el-table-column> |
| 70 | - <el-table-column label="调前金额" width="88" align="right"> | |
| 70 | + <el-table-column label="调前成本总额" width="108" align="right"> | |
| 71 | 71 | <template slot-scope="scope">{{ formatMoney(scope.row.tqje) }}</template> |
| 72 | 72 | </el-table-column> |
| 73 | - <el-table-column label="调后金额" width="88" align="right"> | |
| 73 | + <el-table-column label="调后成本总额" width="108" align="right"> | |
| 74 | 74 | <template slot-scope="scope">{{ formatMoney(scope.row.thje) }}</template> |
| 75 | 75 | </el-table-column> |
| 76 | 76 | </el-table> | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtSp/Form.vue
| ... | ... | @@ -78,23 +78,6 @@ |
| 78 | 78 | </el-form-item> |
| 79 | 79 | </el-col> |
| 80 | 80 | <el-col :xs="24" :sm="8"> |
| 81 | - <el-form-item label="抖音SKU(可多个)" prop="dyspidList"> | |
| 82 | - <el-select | |
| 83 | - :key="'wt-sp-dyspid-' + (dataForm.id || 'new')" | |
| 84 | - v-model="dataForm.dyspidList" | |
| 85 | - multiple | |
| 86 | - filterable | |
| 87 | - allow-create | |
| 88 | - default-first-option | |
| 89 | - collapse-tags | |
| 90 | - placeholder="选填,输入 SKU 后回车添加,可多个" | |
| 91 | - clearable | |
| 92 | - class="wt-sp-form__fullwidth" | |
| 93 | - @change="handleDyspidListChange" | |
| 94 | - /> | |
| 95 | - </el-form-item> | |
| 96 | - </el-col> | |
| 97 | - <el-col :xs="24" :sm="8"> | |
| 98 | 81 | <el-form-item label="序列号类型" prop="spxlhType"> |
| 99 | 82 | <el-select v-model="dataForm.spxlhType" placeholder="请选择" clearable class="wt-sp-form__fullwidth"> |
| 100 | 83 | <el-option |
| ... | ... | @@ -128,6 +111,61 @@ |
| 128 | 111 | <el-input v-model="dataForm.kc" placeholder="根据在库序列号统计,不可修改" readonly /> |
| 129 | 112 | </el-form-item> |
| 130 | 113 | </el-col> |
| 114 | + <el-col :span="24"> | |
| 115 | + <el-form-item label="抖音SKU" prop="dyspidList" class="wt-sp-form-item--stack"> | |
| 116 | + <div class="wt-sp-dyspid-editor"> | |
| 117 | + <div v-if="!dataForm.dyspidList || !dataForm.dyspidList.length" class="wt-sp-dyspid-empty"> | |
| 118 | + <span>该商品暂未配置抖音 SKU</span> | |
| 119 | + <el-button | |
| 120 | + v-if="!isDetail" | |
| 121 | + type="primary" | |
| 122 | + plain | |
| 123 | + size="small" | |
| 124 | + icon="el-icon-plus" | |
| 125 | + @click="addDyspid" | |
| 126 | + >添加抖音 SKU</el-button> | |
| 127 | + </div> | |
| 128 | + <template v-else> | |
| 129 | + <div class="wt-sp-dyspid-list"> | |
| 130 | + <div | |
| 131 | + v-for="(sku, idx) in dataForm.dyspidList" | |
| 132 | + :key="'dyspid_' + idx" | |
| 133 | + class="wt-sp-dyspid-row" | |
| 134 | + > | |
| 135 | + <span class="wt-sp-dyspid-badge">SKU{{ idx + 1 }}</span> | |
| 136 | + <el-input | |
| 137 | + :value="sku" | |
| 138 | + :placeholder="'请输入抖音 SKU ID(第 ' + (idx + 1) + ' 条)'" | |
| 139 | + :disabled="!!isDetail" | |
| 140 | + clearable | |
| 141 | + class="wt-sp-dyspid-input" | |
| 142 | + @input="(v) => updateDyspid(idx, v)" | |
| 143 | + @blur="normalizeDyspidRow(idx)" | |
| 144 | + /> | |
| 145 | + <el-button | |
| 146 | + v-if="!isDetail" | |
| 147 | + type="text" | |
| 148 | + icon="el-icon-delete" | |
| 149 | + class="wt-sp-dyspid-del" | |
| 150 | + title="删除此 SKU" | |
| 151 | + @click="removeDyspid(idx)" | |
| 152 | + /> | |
| 153 | + </div> | |
| 154 | + </div> | |
| 155 | + <div v-if="!isDetail" class="wt-sp-dyspid-footer"> | |
| 156 | + <el-button | |
| 157 | + type="primary" | |
| 158 | + plain | |
| 159 | + size="small" | |
| 160 | + icon="el-icon-plus" | |
| 161 | + @click="addDyspid" | |
| 162 | + >添加抖音 SKU</el-button> | |
| 163 | + <span class="wt-sp-dyspid-hint">已配置 {{ dataForm.dyspidList.length }} 条;抖音后台每个商品规格(SKU)对应一行,可维护多个以支持一品多规;保存时会自动去空与去重。</span> | |
| 164 | + </div> | |
| 165 | + </template> | |
| 166 | + </div> | |
| 167 | + </el-form-item> | |
| 168 | + </el-col> | |
| 131 | 169 | </el-row> |
| 132 | 170 | |
| 133 | 171 | <!-- 二、展示与规则:主图 + 限高详情 + 规则并排 --> |
| ... | ... | @@ -269,6 +307,37 @@ |
| 269 | 307 | hyxz: undefined |
| 270 | 308 | }, |
| 271 | 309 | rules: { |
| 310 | + spbm: [ | |
| 311 | + { | |
| 312 | + validator: (rule, value, callback) => { | |
| 313 | + const code = value == null ? '' : String(value).trim() | |
| 314 | + if (!code) { | |
| 315 | + callback() | |
| 316 | + return | |
| 317 | + } | |
| 318 | + if (this._spbmCheckTimer) clearTimeout(this._spbmCheckTimer) | |
| 319 | + this._spbmCheckTimer = setTimeout(() => { | |
| 320 | + request({ | |
| 321 | + url: '/api/Extend/WtSp/Actions/CheckSpbm', | |
| 322 | + method: 'GET', | |
| 323 | + data: { spbm: code, id: this.dataForm.id || '' } | |
| 324 | + }).then(res => { | |
| 325 | + const data = (res && res.data) || res || {} | |
| 326 | + const payload = data.data || data | |
| 327 | + if (payload && payload.exists) { | |
| 328 | + const who = payload.spmc ? `(${payload.spmc})` : '' | |
| 329 | + callback(new Error(`商品编码已存在${who},请更换`)) | |
| 330 | + } else { | |
| 331 | + callback() | |
| 332 | + } | |
| 333 | + }).catch(() => { | |
| 334 | + callback() | |
| 335 | + }) | |
| 336 | + }, 250) | |
| 337 | + }, | |
| 338 | + trigger: 'blur' | |
| 339 | + } | |
| 340 | + ], | |
| 272 | 341 | tcfs: [{ required: true, message: '请选择提成方式', trigger: 'change' }], |
| 273 | 342 | tcfs_j: [ |
| 274 | 343 | { |
| ... | ... | @@ -364,19 +433,50 @@ |
| 364 | 433 | } |
| 365 | 434 | return out |
| 366 | 435 | }, |
| 367 | - handleDyspidListChange(val) { | |
| 368 | - const next = this.normalizeDyspidList(val) | |
| 369 | - const cur = this.dataForm.dyspidList | |
| 370 | - const needSync = | |
| 371 | - !Array.isArray(cur) || | |
| 372 | - cur.length !== next.length || | |
| 373 | - next.some((s, i) => String(cur[i] == null ? '' : cur[i]).trim() !== s) | |
| 374 | - if (needSync) { | |
| 375 | - this.$nextTick(() => { | |
| 376 | - this.$set(this.dataForm, 'dyspidList', next.slice()) | |
| 377 | - }) | |
| 436 | + /** 新版抖音 SKU 编辑器:逐行输入 */ | |
| 437 | + ensureDyspidListArray() { | |
| 438 | + if (!Array.isArray(this.dataForm.dyspidList)) { | |
| 439 | + this.$set(this.dataForm, 'dyspidList', []) | |
| 440 | + } | |
| 441 | + }, | |
| 442 | + syncDyspidCompat() { | |
| 443 | + const list = Array.isArray(this.dataForm.dyspidList) ? this.dataForm.dyspidList : [] | |
| 444 | + const firstNonEmpty = list.map(v => (v == null ? '' : String(v).trim())).find(v => v !== '') | |
| 445 | + this.dataForm.dyspid = firstNonEmpty || undefined | |
| 446 | + }, | |
| 447 | + /** el-input 输入时同步到指定索引(数组索引直接赋值在 Vue 2 下不是响应式,须用 $set) */ | |
| 448 | + updateDyspid(idx, val) { | |
| 449 | + this.ensureDyspidListArray() | |
| 450 | + this.$set(this.dataForm.dyspidList, idx, val == null ? '' : String(val)) | |
| 451 | + }, | |
| 452 | + /** 添加一行空 SKU */ | |
| 453 | + addDyspid() { | |
| 454 | + this.ensureDyspidListArray() | |
| 455 | + this.dataForm.dyspidList.push('') | |
| 456 | + }, | |
| 457 | + /** 删除一行 SKU */ | |
| 458 | + removeDyspid(idx) { | |
| 459 | + if (!Array.isArray(this.dataForm.dyspidList)) return | |
| 460 | + if (idx < 0 || idx >= this.dataForm.dyspidList.length) return | |
| 461 | + this.dataForm.dyspidList.splice(idx, 1) | |
| 462 | + this.syncDyspidCompat() | |
| 463 | + }, | |
| 464 | + /** 失焦时对当前行做 trim + 去重校验,重复项给出提示 */ | |
| 465 | + normalizeDyspidRow(idx) { | |
| 466 | + if (!Array.isArray(this.dataForm.dyspidList)) return | |
| 467 | + const raw = this.dataForm.dyspidList[idx] | |
| 468 | + const trimmed = raw == null ? '' : String(raw).trim() | |
| 469 | + if (trimmed !== raw) { | |
| 470 | + this.$set(this.dataForm.dyspidList, idx, trimmed) | |
| 471 | + } | |
| 472 | + if (trimmed) { | |
| 473 | + const dupIdx = this.dataForm.dyspidList.findIndex((v, i) => i !== idx && String(v == null ? '' : v).trim() === trimmed) | |
| 474 | + if (dupIdx !== -1) { | |
| 475 | + this.$message.warning('该抖音 SKU 已存在,已自动清空重复项') | |
| 476 | + this.$set(this.dataForm.dyspidList, idx, '') | |
| 477 | + } | |
| 378 | 478 | } |
| 379 | - this.dataForm.dyspid = next.length ? next[0] : undefined | |
| 479 | + this.syncDyspidCompat() | |
| 380 | 480 | }, |
| 381 | 481 | /** 编辑回显:优先 dyspidList(数组或兼容字符串),否则单字段 dyspid */ |
| 382 | 482 | hydrateDyspidFields() { |
| ... | ... | @@ -727,6 +827,70 @@ |
| 727 | 827 | .wt-sp-form-item--quill >>> .el-form-item__content { |
| 728 | 828 | line-height: normal; |
| 729 | 829 | } |
| 830 | +/* 抖音 SKU 编辑器:每行独立可见可操作 */ | |
| 831 | +.wt-sp-dyspid-editor { | |
| 832 | + background: #fafbfc; | |
| 833 | + border: 1px solid #ebeef5; | |
| 834 | + border-radius: 4px; | |
| 835 | + padding: 10px 12px; | |
| 836 | +} | |
| 837 | +.wt-sp-dyspid-empty { | |
| 838 | + display: flex; | |
| 839 | + align-items: center; | |
| 840 | + gap: 12px; | |
| 841 | + color: #909399; | |
| 842 | + font-size: 13px; | |
| 843 | +} | |
| 844 | +.wt-sp-dyspid-list { | |
| 845 | + display: flex; | |
| 846 | + flex-direction: column; | |
| 847 | + gap: 8px; | |
| 848 | +} | |
| 849 | +.wt-sp-dyspid-row { | |
| 850 | + display: flex; | |
| 851 | + align-items: center; | |
| 852 | + gap: 10px; | |
| 853 | +} | |
| 854 | +.wt-sp-dyspid-badge { | |
| 855 | + flex: 0 0 auto; | |
| 856 | + display: inline-flex; | |
| 857 | + align-items: center; | |
| 858 | + justify-content: center; | |
| 859 | + min-width: 52px; | |
| 860 | + height: 28px; | |
| 861 | + padding: 0 10px; | |
| 862 | + background: #ecf5ff; | |
| 863 | + color: #409eff; | |
| 864 | + border-radius: 14px; | |
| 865 | + font-size: 12px; | |
| 866 | + font-weight: 600; | |
| 867 | + letter-spacing: 0.5px; | |
| 868 | +} | |
| 869 | +.wt-sp-dyspid-input { | |
| 870 | + flex: 1 1 auto; | |
| 871 | + min-width: 0; | |
| 872 | +} | |
| 873 | +.wt-sp-dyspid-del { | |
| 874 | + flex: 0 0 auto; | |
| 875 | + color: #f56c6c !important; | |
| 876 | + padding: 4px 6px !important; | |
| 877 | +} | |
| 878 | +.wt-sp-dyspid-del:hover { | |
| 879 | + color: #c45656 !important; | |
| 880 | +} | |
| 881 | +.wt-sp-dyspid-footer { | |
| 882 | + display: flex; | |
| 883 | + align-items: center; | |
| 884 | + gap: 12px; | |
| 885 | + margin-top: 10px; | |
| 886 | + padding-top: 10px; | |
| 887 | + border-top: 1px dashed #e4e7ed; | |
| 888 | +} | |
| 889 | +.wt-sp-dyspid-hint { | |
| 890 | + color: #909399; | |
| 891 | + font-size: 12px; | |
| 892 | + line-height: 1.6; | |
| 893 | +} | |
| 730 | 894 | </style> |
| 731 | 895 | <style> |
| 732 | 896 | /* 弹层:保留足够上内边距,避免首段「档案与定价」等贴顶被裁切;与 NCC-dialog 左右留白协调 */ | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtSp/index.vue
| ... | ... | @@ -99,14 +99,14 @@ |
| 99 | 99 | <screenfull isContainer /> |
| 100 | 100 | </div> |
| 101 | 101 | </div> |
| 102 | - <NCC-table v-loading="listLoading" :data="list" has-c @selection-change="handleSelectionChange"> | |
| 103 | - <el-table-column prop="spmc" label="商品名称" align="left" min-width="260" class-name="wt-sp-spmc-col"> | |
| 102 | + <NCC-table v-loading="listLoading" class="wt-sp-table" :data="list" has-c @selection-change="handleSelectionChange"> | |
| 103 | + <el-table-column prop="spmc" label="商品名称" align="left" min-width="220" show-overflow-tooltip> | |
| 104 | 104 | <template slot-scope="scope"> |
| 105 | 105 | <i class="el-icon-goods wt-sp-col-icon wt-sp-col-icon--primary" aria-hidden="true" /> |
| 106 | 106 | <span>{{ scope.row.spmc || '无' }}</span> |
| 107 | 107 | </template> |
| 108 | 108 | </el-table-column> |
| 109 | - <el-table-column label="商品品类" prop="pl" align="left" min-width="130" show-overflow-tooltip> | |
| 109 | + <el-table-column label="商品品类" prop="pl" align="left" min-width="120" show-overflow-tooltip> | |
| 110 | 110 | <template slot-scope="scope"> |
| 111 | 111 | <i class="el-icon-menu wt-sp-col-icon wt-sp-col-icon--success" aria-hidden="true" /> |
| 112 | 112 | <span>{{ scope.row.pl || '无' }}</span> |
| ... | ... | @@ -124,7 +124,7 @@ |
| 124 | 124 | <span>{{ scope.row.spbm || '无' }}</span> |
| 125 | 125 | </template> |
| 126 | 126 | </el-table-column> |
| 127 | - <el-table-column label="抖音SKU(可多个)" align="left" min-width="160" show-overflow-tooltip> | |
| 127 | + <el-table-column label="抖音SKU(可多个)" align="left" min-width="180" show-overflow-tooltip> | |
| 128 | 128 | <template slot-scope="scope"> |
| 129 | 129 | <i class="el-icon-price-tag wt-sp-col-icon wt-sp-col-icon--info" aria-hidden="true" /> |
| 130 | 130 | <span>{{ formatDyspidListCell(scope.row) }}</span> |
| ... | ... | @@ -136,36 +136,24 @@ |
| 136 | 136 | <span>{{ dynText(scope.row.spxlhType, spxlhTypeOptions) }}</span> |
| 137 | 137 | </template> |
| 138 | 138 | </el-table-column> |
| 139 | - <el-table-column prop="lsj" label="零售价" align="right" width="96" show-overflow-tooltip> | |
| 139 | + <el-table-column prop="lsj" label="零售价" align="right" min-width="96" show-overflow-tooltip> | |
| 140 | 140 | <template slot-scope="scope"> |
| 141 | 141 | <i class="el-icon-coin wt-sp-col-icon wt-sp-col-icon--warning" aria-hidden="true" /> |
| 142 | - <span>{{ scope.row.lsj }}</span> | |
| 142 | + <span>{{ formatLsjCell(scope.row) }}</span> | |
| 143 | 143 | </template> |
| 144 | 144 | </el-table-column> |
| 145 | - <el-table-column prop="zg" label="限购" align="right" width="72" show-overflow-tooltip> | |
| 145 | + <el-table-column prop="zg" label="限购" align="right" min-width="72" show-overflow-tooltip> | |
| 146 | 146 | <template slot-scope="scope"> |
| 147 | 147 | <i class="el-icon-warning-outline wt-sp-col-icon wt-sp-col-icon--info" aria-hidden="true" /> |
| 148 | 148 | <span>{{ scope.row.zg != null && scope.row.zg !== '' ? scope.row.zg : '无' }}</span> |
| 149 | 149 | </template> |
| 150 | 150 | </el-table-column> |
| 151 | - <el-table-column label="实时库存" align="right" width="102" show-overflow-tooltip> | |
| 151 | + <el-table-column label="实时库存" align="right" min-width="96" show-overflow-tooltip> | |
| 152 | 152 | <template slot-scope="scope"> |
| 153 | 153 | <i class="el-icon-box wt-sp-col-icon wt-sp-col-icon--success" aria-hidden="true" /> |
| 154 | 154 | <span>{{ formatRealtimeKc(scope.row) }}</span> |
| 155 | 155 | </template> |
| 156 | 156 | </el-table-column> |
| 157 | - <el-table-column prop="shgz" label="售后规则" align="left" min-width="120" show-overflow-tooltip> | |
| 158 | - <template slot-scope="scope"> | |
| 159 | - <i class="el-icon-service wt-sp-col-icon wt-sp-col-icon--info" aria-hidden="true" /> | |
| 160 | - <span>{{ scope.row.shgz || '无' }}</span> | |
| 161 | - </template> | |
| 162 | - </el-table-column> | |
| 163 | - <el-table-column prop="yfgz" label="运费规则" align="left" min-width="120" show-overflow-tooltip> | |
| 164 | - <template slot-scope="scope"> | |
| 165 | - <i class="el-icon-truck wt-sp-col-icon wt-sp-col-icon--primary" aria-hidden="true" /> | |
| 166 | - <span>{{ scope.row.yfgz || '无' }}</span> | |
| 167 | - </template> | |
| 168 | - </el-table-column> | |
| 169 | 157 | <el-table-column label="销售渠道" prop="xsqd" align="left" min-width="100" show-overflow-tooltip> |
| 170 | 158 | <template slot-scope="scope"> |
| 171 | 159 | <i class="el-icon-s-shop wt-sp-col-icon wt-sp-col-icon--success" aria-hidden="true" /> |
| ... | ... | @@ -417,6 +405,12 @@ |
| 417 | 405 | if (row.kc !== undefined && row.kc !== null && row.kc !== '') return String(row.kc); |
| 418 | 406 | return '0'; |
| 419 | 407 | }, |
| 408 | + formatLsjCell(row) { | |
| 409 | + if (!row || row.lsj === undefined || row.lsj === null || row.lsj === '') return '无'; | |
| 410 | + const n = Number(row.lsj); | |
| 411 | + if (!Number.isNaN(n)) return n.toFixed(2); | |
| 412 | + return String(row.lsj); | |
| 413 | + }, | |
| 420 | 414 | /** 列表展示:优先 dyspidList,兼容旧字段 dyspid */ |
| 421 | 415 | formatDyspidListCell(row) { |
| 422 | 416 | if (!row) return '无' |
| ... | ... | @@ -454,13 +448,9 @@ |
| 454 | 448 | } |
| 455 | 449 | </script> |
| 456 | 450 | <style scoped> |
| 457 | -/* 商品名称列允许多行换行,尽量展示完整(与项目 NCC-table 单行策略区分:仅本列表) */ | |
| 458 | -::v-deep .wt-sp-spmc-col .cell { | |
| 459 | - white-space: normal !important; | |
| 460 | - word-break: break-word; | |
| 461 | - line-height: 1.45; | |
| 462 | - padding-top: 8px; | |
| 463 | - padding-bottom: 8px; | |
| 451 | +/* 列表单元格单行不换行,过长由 show-overflow-tooltip 悬停查看 */ | |
| 452 | +::v-deep .wt-sp-table .el-table .cell { | |
| 453 | + white-space: nowrap; | |
| 464 | 454 | } |
| 465 | 455 | .wt-sp-col-icon { |
| 466 | 456 | margin-right: 6px; | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtTjdbd/Form.vue
| ... | ... | @@ -1516,6 +1516,18 @@ |
| 1516 | 1516 | return label.includes(query.toLowerCase()); |
| 1517 | 1517 | }, |
| 1518 | 1518 | async handleProductChange(row) { |
| 1519 | + if (!row.spbh) { | |
| 1520 | + this.$set(row, 'spmc', '') | |
| 1521 | + this.$set(row, 'dw', '') | |
| 1522 | + this.$set(row, 'sl', undefined) | |
| 1523 | + this.$set(row, 'dj', undefined) | |
| 1524 | + this.$set(row, 'je', undefined) | |
| 1525 | + this.$set(row, 'description', '') | |
| 1526 | + this.$set(row, 'kucun', undefined) | |
| 1527 | + this.$set(row, 'spxlhLoaded', false) | |
| 1528 | + this.$set(row, 'xlhList', []) | |
| 1529 | + return | |
| 1530 | + } | |
| 1519 | 1531 | // 选中商品后可自动回填商品名称等信息 |
| 1520 | 1532 | const product = this.spbhOptions.find(item => item.F_Id === row.spbh); |
| 1521 | 1533 | if (product) { | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtXsckd/Form.vue
| ... | ... | @@ -252,18 +252,6 @@ |
| 252 | 252 | </el-input> |
| 253 | 253 | </el-form-item> |
| 254 | 254 | </el-col> |
| 255 | - <el-col :span="8"> | |
| 256 | - <el-form-item label="审核人" prop="shr"> | |
| 257 | - <user-select v-model="dataForm.shr" placeholder="请选择" clearable > | |
| 258 | - </user-select> | |
| 259 | - </el-form-item> | |
| 260 | - </el-col> | |
| 261 | - <el-col :span="8"> | |
| 262 | - <el-form-item label="过账人" prop="gzr"> | |
| 263 | - <user-select v-model="dataForm.gzr" placeholder="请选择" clearable > | |
| 264 | - </user-select> | |
| 265 | - </el-form-item> | |
| 266 | - </el-col> | |
| 267 | 255 | <el-col :span="24"> |
| 268 | 256 | <el-form-item label="备注" prop="bz"> |
| 269 | 257 | <el-input v-model="dataForm.bz" placeholder="请输入" show-word-limit :style='{"width":"100%"}' type='textarea' :autosize='{"minRows":4,"maxRows":4}' > |
| ... | ... | @@ -725,17 +713,7 @@ |
| 725 | 713 | else{ |
| 726 | 714 | _this.dataForm.ly = '后台'; |
| 727 | 715 | _this.dataForm.djlx = '销售出库单'; |
| 728 | - // 新建时加载默认选项设置 | |
| 729 | - request({ url: '/api/Extend/WtMrsz', method: 'get' }).then(res => { | |
| 730 | - if (res.data) { | |
| 731 | - if (res.data.mrck) { | |
| 732 | - _this.dataForm.cjck = res.data.mrck | |
| 733 | - _this.dataForm.cjckId = res.data.mrck | |
| 734 | - } | |
| 735 | - if (res.data.mrwldw) _this.dataForm.kh = res.data.mrwldw | |
| 736 | - if (res.data.mrskzh) _this.dataForm.skzh = res.data.mrskzh | |
| 737 | - } | |
| 738 | - }).catch(() => {}) | |
| 716 | + // 新建销售出库单:不再加载默认仓库/往来单位/收款账户,由用户手动选择 | |
| 739 | 717 | } |
| 740 | 718 | }) |
| 741 | 719 | }, |
| ... | ... | @@ -1583,7 +1561,12 @@ |
| 1583 | 1561 | this.$set(row, 'cbje', undefined) |
| 1584 | 1562 | this.$set(row, 'dj', undefined) |
| 1585 | 1563 | this.$set(row, 'je', undefined) |
| 1564 | + this.$set(row, 'sl', undefined) | |
| 1586 | 1565 | this.$set(row, 'spmc', '') |
| 1566 | + this.$set(row, 'description', '') | |
| 1567 | + this.$set(row, 'kucun', undefined) | |
| 1568 | + this.$set(row, 'spxlhLoaded', false) | |
| 1569 | + this.$set(row, 'xlhList', []) | |
| 1587 | 1570 | return |
| 1588 | 1571 | } |
| 1589 | 1572 | // 选中商品后可自动回填商品名称等信息 | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtXsckd/detail-view.vue
| ... | ... | @@ -73,6 +73,12 @@ |
| 73 | 73 | {{ cellText(detail.jsr) }} |
| 74 | 74 | </span> |
| 75 | 75 | </el-descriptions-item> |
| 76 | + <el-descriptions-item v-if="detail.fhr" label="发货人"> | |
| 77 | + <span class="cell-nowrap"> | |
| 78 | + <i class="el-icon-truck desc-icon desc-icon--primary" /> | |
| 79 | + {{ cellText(detail.fhr) }} | |
| 80 | + </span> | |
| 81 | + </el-descriptions-item> | |
| 76 | 82 | <el-descriptions-item label="会员手机"> |
| 77 | 83 | <span class="cell-nowrap"> |
| 78 | 84 | <i class="el-icon-phone desc-icon desc-icon--info" /> |
| ... | ... | @@ -366,11 +372,11 @@ export default { |
| 366 | 372 | displayCollectionRaw() { |
| 367 | 373 | const row = this.detail |
| 368 | 374 | if (!row) return 0 |
| 369 | - const hasYdje = row.ydje != null && row.ydje !== '' | |
| 370 | - if (hasYdje) { | |
| 371 | - const ydje = parseFloat(row.ydje) | |
| 372 | - const ysje = parseFloat(row.ysje) || 0 | |
| 373 | - return !isNaN(ydje) ? ydje - ysje : 0 | |
| 375 | + // 与列表页保持一致:优先使用数据库真实存储的 skje, | |
| 376 | + // 兼容抖音单(用户支付≠商家实收)的场景;skje 缺失再回退 | |
| 377 | + if (row.skje != null && row.skje !== '') { | |
| 378 | + const skje = parseFloat(row.skje) | |
| 379 | + if (!isNaN(skje)) return skje | |
| 374 | 380 | } |
| 375 | 381 | if (row.skmx || row.fkmx) { |
| 376 | 382 | let sum = 0 |
| ... | ... | @@ -384,7 +390,11 @@ export default { |
| 384 | 390 | } |
| 385 | 391 | if (sum > 0) return sum |
| 386 | 392 | } |
| 387 | - if (row.skje != null && row.skje !== '') return parseFloat(row.skje) | |
| 393 | + if (row.ydje != null && row.ydje !== '') { | |
| 394 | + const ydje = parseFloat(row.ydje) | |
| 395 | + const ysje = parseFloat(row.ysje) || 0 | |
| 396 | + if (!isNaN(ydje)) return ydje - ysje | |
| 397 | + } | |
| 388 | 398 | return 0 |
| 389 | 399 | }, |
| 390 | 400 | displayCollectionAmount() { | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtXsckd/index.vue
| ... | ... | @@ -74,15 +74,6 @@ |
| 74 | 74 | </el-form-item> |
| 75 | 75 | </el-col> |
| 76 | 76 | <el-col :span="6"> |
| 77 | - <el-form-item label="单据类型"> | |
| 78 | - <el-select v-model="query.djlx" placeholder="单据类型" clearable> | |
| 79 | - <el-option label="销售出库单" value="销售出库单" /> | |
| 80 | - <el-option label="预售出库单" value="预售出库单" /> | |
| 81 | - </el-select> | |
| 82 | - </el-form-item> | |
| 83 | - </el-col> | |
| 84 | - | |
| 85 | - <el-col :span="6"> | |
| 86 | 77 | <el-form-item label="单据来源"> |
| 87 | 78 | <el-select v-model="query.ly" placeholder="单据来源" clearable> |
| 88 | 79 | <el-option label="后台" value="后台" /> |
| ... | ... | @@ -131,6 +122,9 @@ |
| 131 | 122 | <el-table-column prop="jsr" label="经手人" align="left" min-width="88" show-overflow-tooltip> |
| 132 | 123 | <template slot-scope="scope">{{ displayText(scope.row.jsr) }}</template> |
| 133 | 124 | </el-table-column> |
| 125 | + <el-table-column prop="fhr" label="发货人" align="left" min-width="88" show-overflow-tooltip> | |
| 126 | + <template slot-scope="scope">{{ displayText(scope.row.fhr) }}</template> | |
| 127 | + </el-table-column> | |
| 134 | 128 | <el-table-column prop="hysjh" label="会员手机号码" align="left" min-width="120" show-overflow-tooltip> |
| 135 | 129 | <template slot-scope="scope">{{ displayText(scope.row.hysjh) }}</template> |
| 136 | 130 | </el-table-column> |
| ... | ... | @@ -155,19 +149,18 @@ |
| 155 | 149 | <el-table-column prop="bz" label="备注" align="left" min-width="120" show-overflow-tooltip> |
| 156 | 150 | <template slot-scope="scope">{{ displayText(scope.row.bz) }}</template> |
| 157 | 151 | </el-table-column> |
| 158 | - <el-table-column prop="yddh" label="运单号" align="left" min-width="130" show-overflow-tooltip> | |
| 159 | - <template slot-scope="scope">{{ displayText(scope.row.yddh) }}</template> | |
| 160 | - </el-table-column> | |
| 161 | - <el-table-column prop="dyddh" label="抖音订单号" align="left" min-width="150" show-overflow-tooltip> | |
| 152 | + <el-table-column prop="dyddh" label="抖音订单号" align="left" min-width="160" show-overflow-tooltip> | |
| 162 | 153 | <template slot-scope="scope">{{ displayText(scope.row.dyddh) }}</template> |
| 163 | 154 | </el-table-column> |
| 155 | + <el-table-column prop="yddh" label="物流运单号" align="left" min-width="140" show-overflow-tooltip> | |
| 156 | + <template slot-scope="scope">{{ displayText(scope.row.yddh) }}</template> | |
| 157 | + </el-table-column> | |
| 164 | 158 | <el-table-column prop="sy_pch" label="收银批次" align="left" min-width="120" show-overflow-tooltip /> |
| 165 | 159 | <el-table-column label="摘要" align="left" min-width="200" show-overflow-tooltip> |
| 166 | 160 | <template slot-scope="scope"> |
| 167 | 161 | <ncc-table-summary-cell :row="scope.row" fields="zy,Zy" /> |
| 168 | 162 | </template> |
| 169 | 163 | </el-table-column> |
| 170 | - <el-table-column prop="djlx" label="单据类型" align="left" min-width="100" show-overflow-tooltip /> | |
| 171 | 164 | <el-table-column label="单据来源" prop="ly" align="left" min-width="110" show-overflow-tooltip> |
| 172 | 165 | <template slot-scope="scope">{{ formatLy(scope.row.ly) }}</template> |
| 173 | 166 | </el-table-column> |
| ... | ... | @@ -250,7 +243,7 @@ |
| 250 | 243 | currentPage: 1, |
| 251 | 244 | pageSize: 20, |
| 252 | 245 | sort: "desc", |
| 253 | - sidx: "", | |
| 246 | + sidx: "id", | |
| 254 | 247 | }, |
| 255 | 248 | formVisible: false, |
| 256 | 249 | detailVisible: false, |
| ... | ... | @@ -260,17 +253,17 @@ |
| 260 | 253 | { prop: 'djrq', label: '单据日期' }, |
| 261 | 254 | { prop: 'cjck', label: '出库仓库' }, |
| 262 | 255 | { prop: 'jsr', label: '经手人' }, |
| 256 | + { prop: 'fhr', label: '发货人' }, | |
| 263 | 257 | { prop: 'skzh', label: '收款账户' }, |
| 264 | 258 | { prop: 'skje', label: '收款金额' }, |
| 265 | 259 | { prop: 'ysje', label: '优惠金额' }, |
| 266 | 260 | { prop: 'shr', label: '审核人' }, |
| 267 | 261 | { prop: 'gzr', label: '过账人' }, |
| 268 | 262 | { prop: 'bz', label: '备注' }, |
| 269 | - { prop: 'yddh', label: '运单号' }, | |
| 270 | 263 | { prop: 'dyddh', label: '抖音订单号' }, |
| 264 | + { prop: 'yddh', label: '物流运单号' }, | |
| 271 | 265 | { prop: 'sy_pch', label: '收银批次' }, |
| 272 | 266 | { prop: 'zy', label: '摘要' }, |
| 273 | - { prop: 'djlx', label: '单据类型' }, | |
| 274 | 267 | { prop: 'thdh', label: '关联退货单号' }, |
| 275 | 268 | ], |
| 276 | 269 | cjckOptions : [], |
| ... | ... | @@ -378,13 +371,14 @@ |
| 378 | 371 | const ysje = parseFloat(row.ysje) || 0; |
| 379 | 372 | return ysje.toFixed(2); |
| 380 | 373 | }, |
| 381 | - // ✅ 获取显示的收款金额(与编辑页一致:有原价时 收款=原价-优惠,否则 skmx 汇总或 skje) | |
| 374 | + // ✅ 获取显示的收款金额 | |
| 375 | + // 优先使用后端存储的真实收款金额 skje(抖音单会出现 skje ≠ ydje-ysje 的情况: | |
| 376 | + // 用户支付 49,但商家实收 9.9,平台扣费 39.1,这时必须看 skje 而不是 ydje-ysje) | |
| 377 | + // skje 缺失时才回退到 skmx/fkmx 汇总,最后才回退到老口径 ydje-ysje,保持对旧数据兼容 | |
| 382 | 378 | getDisplayCollectionAmount(row) { |
| 383 | - const hasYdje = row.ydje != null && row.ydje !== ''; | |
| 384 | - if (hasYdje) { | |
| 385 | - const ydje = parseFloat(row.ydje); | |
| 386 | - const ysje = parseFloat(row.ysje) || 0; | |
| 387 | - return (!isNaN(ydje) ? (ydje - ysje).toFixed(2) : null) || '0.00'; | |
| 379 | + if (row.skje != null && row.skje !== '') { | |
| 380 | + const skje = parseFloat(row.skje); | |
| 381 | + if (!isNaN(skje)) return skje.toFixed(2); | |
| 388 | 382 | } |
| 389 | 383 | if (row.skmx || row.fkmx) { |
| 390 | 384 | try { |
| ... | ... | @@ -406,7 +400,12 @@ |
| 406 | 400 | console.error('解析组合支付明细失败:', e); |
| 407 | 401 | } |
| 408 | 402 | } |
| 409 | - return (row.skje != null && row.skje !== '') ? parseFloat(row.skje).toFixed(2) : '0.00'; | |
| 403 | + if (row.ydje != null && row.ydje !== '') { | |
| 404 | + const ydje = parseFloat(row.ydje); | |
| 405 | + const ysje = parseFloat(row.ysje) || 0; | |
| 406 | + if (!isNaN(ydje)) return (ydje - ysje).toFixed(2); | |
| 407 | + } | |
| 408 | + return '0.00'; | |
| 410 | 409 | }, |
| 411 | 410 | getcjckOptions(){ |
| 412 | 411 | previewDataInterface('681758216954053893').then(res => { |
| ... | ... | @@ -441,6 +440,8 @@ |
| 441 | 440 | query[key] = _query[key] |
| 442 | 441 | } |
| 443 | 442 | } |
| 443 | + // 本页为"销售出库单"专属页,强制只查 销售出库单(不含 预售/退货/代销等同表其他单据) | |
| 444 | + query.djlx = '销售出库单' | |
| 444 | 445 | request({ |
| 445 | 446 | url: `/api/Extend/WtXsckd`, |
| 446 | 447 | method: 'GET', |
| ... | ... | @@ -599,7 +600,7 @@ |
| 599 | 600 | currentPage: 1, |
| 600 | 601 | pageSize: 20, |
| 601 | 602 | sort: "desc", |
| 602 | - sidx: "", | |
| 603 | + sidx: "id", | |
| 603 | 604 | } |
| 604 | 605 | this.initData() |
| 605 | 606 | }, |
| ... | ... | @@ -616,7 +617,7 @@ |
| 616 | 617 | currentPage: 1, |
| 617 | 618 | pageSize: 20, |
| 618 | 619 | sort: 'desc', |
| 619 | - sidx: '' | |
| 620 | + sidx: 'id' | |
| 620 | 621 | } |
| 621 | 622 | this.initData() |
| 622 | 623 | } | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtXsthd/Form.vue
| ... | ... | @@ -612,6 +612,18 @@ |
| 612 | 612 | this.updateTotalAmount(); |
| 613 | 613 | }, |
| 614 | 614 | async handleProductChange(row) { |
| 615 | + if (!row.spbh) { | |
| 616 | + this.$set(row, 'spmc', '') | |
| 617 | + this.$set(row, 'sptm', '') | |
| 618 | + this.$set(row, 'dw', '') | |
| 619 | + this.$set(row, 'sl', undefined) | |
| 620 | + this.$set(row, 'dj', undefined) | |
| 621 | + this.$set(row, 'je', undefined) | |
| 622 | + this.$set(row, 'description', '') | |
| 623 | + this.$set(row, 'spxlhLoaded', false) | |
| 624 | + this.$set(row, 'xlhList', []) | |
| 625 | + return | |
| 626 | + } | |
| 615 | 627 | const product = this.spbhOptions.find(item => item.F_Id === row.spbh); |
| 616 | 628 | console.log('选中商品', row.spbh, product); |
| 617 | 629 | if (product) { | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtXsthd/detail-view.vue
| ... | ... | @@ -164,7 +164,7 @@ |
| 164 | 164 | <el-descriptions-item label="退款账户"> |
| 165 | 165 | <span class="cell-nowrap"> |
| 166 | 166 | <i class="el-icon-bank-card desc-icon desc-icon--success" /> |
| 167 | - {{ labelFromOptions(detail.skzh, skzhOptions) }} | |
| 167 | + {{ displaySkzh() }} | |
| 168 | 168 | </span> |
| 169 | 169 | </el-descriptions-item> |
| 170 | 170 | <el-descriptions-item label="审核人"> |
| ... | ... | @@ -207,6 +207,7 @@ import { previewDataInterface } from '@/api/systemData/dataInterface' |
| 207 | 207 | import { dynamicText } from '@/filters' |
| 208 | 208 | import XsckdDetailView from '../wtXsckd/detail-view' |
| 209 | 209 | import { getAccountSelector } from '@/api/extend/wtAccount' |
| 210 | +import { formatWtSkzhDisplay } from '@/utils/wtComboSkzhDisplay' | |
| 210 | 211 | |
| 211 | 212 | export default { |
| 212 | 213 | name: 'WtXsthdDetailView', |
| ... | ... | @@ -267,6 +268,10 @@ export default { |
| 267 | 268 | if (this.$refs.XsckdDetailView) this.$refs.XsckdDetailView.init(id) |
| 268 | 269 | }) |
| 269 | 270 | }, |
| 271 | + displaySkzh() { | |
| 272 | + if (!this.detail) return '无' | |
| 273 | + return formatWtSkzhDisplay(this.detail, this.skzhOptions, { mode: 'refund' }) || '无' | |
| 274 | + }, | |
| 270 | 275 | labelFromOptions(value, options) { |
| 271 | 276 | if (value === null || value === undefined || value === '') return '无' |
| 272 | 277 | const opts = options || [] | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtXsthd/index.vue
| ... | ... | @@ -94,8 +94,8 @@ |
| 94 | 94 | <screenfull isContainer /> |
| 95 | 95 | </div> |
| 96 | 96 | </div> |
| 97 | - <NCC-table v-loading="listLoading" :data="list" has-c @selection-change="handleSelectionChange"> | |
| 98 | - <el-table-column prop="id" label="单据编号" align="left" /> | |
| 97 | + <NCC-table v-loading="listLoading" class="wt-xsthd-table" :data="list" has-c @selection-change="handleSelectionChange"> | |
| 98 | + <el-table-column prop="id" label="单据编号" align="left" min-width="150" show-overflow-tooltip /> | |
| 99 | 99 | <el-table-column label="销售出库单号" align="left" min-width="150" show-overflow-tooltip> |
| 100 | 100 | <template slot-scope="scope"> |
| 101 | 101 | <span class="cell-nowrap" @click.stop> |
| ... | ... | @@ -110,22 +110,20 @@ |
| 110 | 110 | </span> |
| 111 | 111 | </template> |
| 112 | 112 | </el-table-column> |
| 113 | - <el-table-column prop="djrq" label="单据日期" align="left" :formatter="ncc.tableDateFormat" /> | |
| 114 | - <el-table-column label="入库仓库" prop="cjck" align="left"> | |
| 113 | + <el-table-column prop="djrq" label="单据日期" align="left" min-width="110" :formatter="ncc.tableDateFormat" show-overflow-tooltip /> | |
| 114 | + <el-table-column label="入库仓库" prop="cjck" align="left" min-width="120" show-overflow-tooltip> | |
| 115 | 115 | <template slot-scope="scope">{{ formatListCjck(scope.row) }}</template> |
| 116 | 116 | </el-table-column> |
| 117 | - <el-table-column prop="jsr" label="经手人" align="left" /> | |
| 118 | - <!-- <el-table-column prop="ysje" label="优惠金额" align="left" /> --> | |
| 119 | - <el-table-column label="退款账户" prop="skzh" align="left"> | |
| 120 | - <template slot-scope="scope">{{ scope.row.skzh | dynamicText(skzhOptions) }}</template> | |
| 117 | + <el-table-column prop="jsr" label="经手人" align="left" min-width="88" show-overflow-tooltip /> | |
| 118 | + <el-table-column label="退款账户" prop="skzh" align="left" min-width="220" show-overflow-tooltip> | |
| 119 | + <template slot-scope="scope">{{ formatSkzhRow(scope.row) }}</template> | |
| 121 | 120 | </el-table-column> |
| 122 | - <el-table-column prop="skje" label="退款金额" align="left" /> | |
| 123 | - <!-- <el-table-column prop="zdr" label="制单人" align="left" /> --> | |
| 124 | - <el-table-column prop="shr" label="审核人" align="left" /> | |
| 125 | - <el-table-column prop="gzr" label="过账人" align="left" /> | |
| 126 | - <el-table-column prop="bz" label="备注" align="left" /> | |
| 127 | - <el-table-column prop="djlx" label="单据类型" align="left" /> | |
| 128 | - <el-table-column prop="djzt" label="审核状态" align="left"> | |
| 121 | + <el-table-column prop="skje" label="退款金额" align="right" min-width="100" show-overflow-tooltip /> | |
| 122 | + <el-table-column prop="shr" label="审核人" align="left" min-width="88" show-overflow-tooltip /> | |
| 123 | + <el-table-column prop="gzr" label="过账人" align="left" min-width="88" show-overflow-tooltip /> | |
| 124 | + <el-table-column prop="bz" label="备注" align="left" min-width="160" show-overflow-tooltip /> | |
| 125 | + <el-table-column prop="djlx" label="单据类型" align="left" min-width="110" show-overflow-tooltip /> | |
| 126 | + <el-table-column prop="djzt" label="审核状态" align="left" min-width="110" show-overflow-tooltip> | |
| 129 | 127 | <template slot-scope="scope"> |
| 130 | 128 | <el-tag v-if="scope.row.djzt === '已审核'" type="success">已审核</el-tag> |
| 131 | 129 | <el-tag v-else-if="scope.row.djzt === '一级已审'" type="">一级已审</el-tag> |
| ... | ... | @@ -133,7 +131,7 @@ |
| 133 | 131 | <el-tag v-else type="warning">{{ scope.row.djzt || '待审核' }}</el-tag> |
| 134 | 132 | </template> |
| 135 | 133 | </el-table-column> |
| 136 | - <el-table-column label="摘要" align="left" min-width="200" show-overflow-tooltip class-name="cell-nowrap"> | |
| 134 | + <el-table-column label="摘要" align="left" min-width="200" show-overflow-tooltip> | |
| 137 | 135 | <template slot-scope="scope"> |
| 138 | 136 | <ncc-table-summary-cell :row="scope.row" fields="zy,Zy" /> |
| 139 | 137 | </template> |
| ... | ... | @@ -168,6 +166,7 @@ |
| 168 | 166 | import { previewDataInterface } from '@/api/systemData/dataInterface' |
| 169 | 167 | import { promptApprovalRemark, postApproveGeneric, postRejectGeneric } from '@/utils/wtRejectApproval' |
| 170 | 168 | import { getAccountSelector } from '@/api/extend/wtAccount' |
| 169 | + import { formatWtSkzhDisplay } from '@/utils/wtComboSkzhDisplay' | |
| 171 | 170 | export default { |
| 172 | 171 | components: { NCCForm, DetailView, XsckdDetailView, ExportBox }, |
| 173 | 172 | data() { |
| ... | ... | @@ -284,6 +283,9 @@ |
| 284 | 283 | this.skzhOptions = res.data.list |
| 285 | 284 | }); |
| 286 | 285 | }, |
| 286 | + formatSkzhRow(row) { | |
| 287 | + return formatWtSkzhDisplay(row, this.skzhOptions, { mode: 'refund' }) | |
| 288 | + }, | |
| 287 | 289 | initData() { |
| 288 | 290 | this.listLoading = true; |
| 289 | 291 | let _query = { |
| ... | ... | @@ -478,4 +480,9 @@ |
| 478 | 480 | .ycddh-link { |
| 479 | 481 | font-weight: normal; |
| 480 | 482 | } |
| 483 | +.wt-xsthd-table { | |
| 484 | + ::v-deep .el-table .cell { | |
| 485 | + white-space: nowrap; | |
| 486 | + } | |
| 487 | +} | |
| 481 | 488 | </style> |
| 482 | 489 | \ No newline at end of file | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtXswtdxfhd/Form.vue
| ... | ... | @@ -1480,7 +1480,9 @@ |
| 1480 | 1480 | row.dj = undefined; |
| 1481 | 1481 | row.je = undefined; |
| 1482 | 1482 | row.dqxsj = undefined; |
| 1483 | + row.description = undefined; | |
| 1483 | 1484 | this.$set(row, 'selectedSerialNumbers', []); |
| 1485 | + this.$set(row, 'xlhList', []); | |
| 1484 | 1486 | this.$set(row, 'productQuery', ''); |
| 1485 | 1487 | row.loadingStock = false; |
| 1486 | 1488 | this.$set(row, 'spxlhLoaded', false); | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtXswtdxjsd/Form.vue
| ... | ... | @@ -1405,6 +1405,17 @@ |
| 1405 | 1405 | return label.includes(query.toLowerCase()); |
| 1406 | 1406 | }, |
| 1407 | 1407 | handleProductChange(row) { |
| 1408 | + if (!row.spbh) { | |
| 1409 | + this.$set(row, 'spmc', '') | |
| 1410 | + this.$set(row, 'sl', undefined) | |
| 1411 | + this.$set(row, 'dj', undefined) | |
| 1412 | + this.$set(row, 'je', undefined) | |
| 1413 | + this.$set(row, 'description', '') | |
| 1414 | + this.$set(row, 'kucun', undefined) | |
| 1415 | + this.$set(row, 'spxlhLoaded', false) | |
| 1416 | + this.$set(row, 'xlhList', []) | |
| 1417 | + return | |
| 1418 | + } | |
| 1408 | 1419 | // 选中商品后可自动回填商品名称等信息 |
| 1409 | 1420 | const product = this.spbhOptions.find(item => item.F_Id === row.spbh); |
| 1410 | 1421 | if (product) { | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtXswtdxthd/Form.vue
| ... | ... | @@ -647,6 +647,18 @@ |
| 647 | 647 | this.updateTotalAmount(); |
| 648 | 648 | }, |
| 649 | 649 | async handleProductChange(row) { |
| 650 | + if (!row.spbh) { | |
| 651 | + this.$set(row, 'spmc', '') | |
| 652 | + this.$set(row, 'sptm', '') | |
| 653 | + this.$set(row, 'dw', '') | |
| 654 | + this.$set(row, 'sl', undefined) | |
| 655 | + this.$set(row, 'dj', undefined) | |
| 656 | + this.$set(row, 'je', undefined) | |
| 657 | + this.$set(row, 'description', '') | |
| 658 | + this.$set(row, 'spxlhLoaded', false) | |
| 659 | + this.$set(row, 'xlhList', []) | |
| 660 | + return | |
| 661 | + } | |
| 650 | 662 | const product = this.spbhOptions.find(item => item.F_Id === row.spbh); |
| 651 | 663 | console.log('选中商品', row.spbh, product); |
| 652 | 664 | if (product) { | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtYsckd/Form.vue
| ... | ... | @@ -1434,6 +1434,17 @@ |
| 1434 | 1434 | return label.includes(query.toLowerCase()); |
| 1435 | 1435 | }, |
| 1436 | 1436 | handleProductChange(row) { |
| 1437 | + if (!row.spbh) { | |
| 1438 | + this.$set(row, 'spmc', '') | |
| 1439 | + this.$set(row, 'sl', undefined) | |
| 1440 | + this.$set(row, 'dj', undefined) | |
| 1441 | + this.$set(row, 'je', undefined) | |
| 1442 | + this.$set(row, 'description', '') | |
| 1443 | + this.$set(row, 'kucun', undefined) | |
| 1444 | + this.$set(row, 'spxlhLoaded', false) | |
| 1445 | + this.$set(row, 'xlhList', []) | |
| 1446 | + return | |
| 1447 | + } | |
| 1437 | 1448 | // 选中商品后可自动回填商品名称等信息 |
| 1438 | 1449 | const product = this.spbhOptions.find(item => item.F_Id === row.spbh); |
| 1439 | 1450 | if (product) { | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtYsckd/index.vue
| ... | ... | @@ -116,38 +116,38 @@ |
| 116 | 116 | <screenfull isContainer /> |
| 117 | 117 | </div> |
| 118 | 118 | </div> |
| 119 | - <NCC-table v-loading="listLoading" :data="list" has-c @selection-change="handleSelectionChange"> | |
| 120 | - <el-table-column prop="id" label="单据编号" align="left" /> | |
| 121 | - <el-table-column prop="djrq" label="单据日期" align="left" :formatter="ncc.tableDateFormat" /> | |
| 122 | - <el-table-column label="出库仓库" prop="cjck" align="left"> | |
| 119 | + <NCC-table v-loading="listLoading" class="wt-ysckd-table" :data="list" has-c @selection-change="handleSelectionChange"> | |
| 120 | + <el-table-column prop="id" label="单据编号" align="left" min-width="150" show-overflow-tooltip /> | |
| 121 | + <el-table-column prop="djrq" label="单据日期" align="left" min-width="110" :formatter="ncc.tableDateFormat" show-overflow-tooltip /> | |
| 122 | + <el-table-column label="出库仓库" prop="cjck" align="left" min-width="120" show-overflow-tooltip> | |
| 123 | 123 | <template slot-scope="scope">{{ scope.row.cjck | dynamicText(cjckOptions) }}</template> |
| 124 | 124 | </el-table-column> |
| 125 | - <el-table-column prop="jsr" label="经手人" align="left" /> | |
| 126 | - <el-table-column prop="hysjh" label="会员手机号码" align="left" /> | |
| 127 | - <el-table-column label="原价" align="left" min-width="90"> | |
| 125 | + <el-table-column prop="jsr" label="经手人" align="left" min-width="88" show-overflow-tooltip /> | |
| 126 | + <el-table-column prop="hysjh" label="会员手机号码" align="left" min-width="120" show-overflow-tooltip /> | |
| 127 | + <el-table-column label="原价" align="right" min-width="96" show-overflow-tooltip> | |
| 128 | 128 | <template slot-scope="scope">{{ getDisplayOriginalPrice(scope.row) }}</template> |
| 129 | 129 | </el-table-column> |
| 130 | - <el-table-column label="收款金额" align="left" min-width="90"> | |
| 130 | + <el-table-column label="收款金额" align="right" min-width="96" show-overflow-tooltip> | |
| 131 | 131 | <template slot-scope="scope"> |
| 132 | 132 | {{ getDisplayCollectionAmount(scope.row) }} |
| 133 | 133 | </template> |
| 134 | 134 | </el-table-column> |
| 135 | - <el-table-column label="优惠金额" align="left" min-width="90"> | |
| 135 | + <el-table-column label="优惠金额" align="right" min-width="96" show-overflow-tooltip> | |
| 136 | 136 | <template slot-scope="scope">{{ getDisplayDiscountAmount(scope.row) }}</template> |
| 137 | 137 | </el-table-column> |
| 138 | - <el-table-column label="收款账户" prop="skzh" align="left"> | |
| 138 | + <el-table-column label="收款账户" prop="skzh" align="left" min-width="220" show-overflow-tooltip> | |
| 139 | 139 | <template slot-scope="scope">{{ formatSkzhRow(scope.row) }}</template> |
| 140 | 140 | </el-table-column> |
| 141 | - <el-table-column prop="zdr" label="制单人" align="left" /> | |
| 142 | - <el-table-column prop="shr" label="审核人" align="left" /> | |
| 143 | - <el-table-column prop="gzr" label="过账人" align="left" /> | |
| 144 | - <el-table-column prop="bz" label="备注" align="left" /> | |
| 141 | + <el-table-column prop="zdr" label="制单人" align="left" min-width="88" show-overflow-tooltip /> | |
| 142 | + <el-table-column prop="shr" label="审核人" align="left" min-width="88" show-overflow-tooltip /> | |
| 143 | + <el-table-column prop="gzr" label="过账人" align="left" min-width="88" show-overflow-tooltip /> | |
| 144 | + <el-table-column prop="bz" label="备注" align="left" min-width="160" show-overflow-tooltip /> | |
| 145 | 145 | <el-table-column prop="sy_pch" label="收银批次" align="left" min-width="120" show-overflow-tooltip /> |
| 146 | - <el-table-column prop="djlx" label="单据类型" align="left" /> | |
| 147 | - <el-table-column label="单据来源" prop="ly" align="left" min-width="100"> | |
| 146 | + <el-table-column prop="djlx" label="单据类型" align="left" min-width="110" show-overflow-tooltip /> | |
| 147 | + <el-table-column label="单据来源" prop="ly" align="left" min-width="110" show-overflow-tooltip> | |
| 148 | 148 | <template slot-scope="scope">{{ formatLy(scope.row.ly) }}</template> |
| 149 | 149 | </el-table-column> |
| 150 | - <el-table-column prop="djzt" label="审核状态" align="left"> | |
| 150 | + <el-table-column prop="djzt" label="审核状态" align="left" min-width="110" show-overflow-tooltip> | |
| 151 | 151 | <template slot-scope="scope"> |
| 152 | 152 | <el-tag v-if="scope.row.djzt === '已审核'" type="success">已审核</el-tag> |
| 153 | 153 | <el-tag v-else-if="scope.row.djzt === '一级已审'" type="">一级已审</el-tag> |
| ... | ... | @@ -157,7 +157,7 @@ |
| 157 | 157 | <el-tag v-else type="warning">{{ scope.row.djzt || '待审核' }}</el-tag> |
| 158 | 158 | </template> |
| 159 | 159 | </el-table-column> |
| 160 | - <el-table-column label="摘要" align="left" min-width="200" show-overflow-tooltip class-name="cell-nowrap"> | |
| 160 | + <el-table-column label="摘要" align="left" min-width="200" show-overflow-tooltip> | |
| 161 | 161 | <template slot-scope="scope"> |
| 162 | 162 | <ncc-table-summary-cell :row="scope.row" fields="zy,Zy" /> |
| 163 | 163 | </template> |
| ... | ... | @@ -743,4 +743,8 @@ |
| 743 | 743 | .convert-button-wrapper .el-button { |
| 744 | 744 | pointer-events: auto !important; |
| 745 | 745 | } |
| 746 | + | |
| 747 | + .wt-ysckd-table ::v-deep .el-table .cell { | |
| 748 | + white-space: nowrap; | |
| 749 | + } | |
| 746 | 750 | </style> |
| 747 | 751 | \ No newline at end of file | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtYsthd/Form.vue
| ... | ... | @@ -603,6 +603,18 @@ |
| 603 | 603 | this.updateTotalAmount(); |
| 604 | 604 | }, |
| 605 | 605 | async handleProductChange(row) { |
| 606 | + if (!row.spbh) { | |
| 607 | + this.$set(row, 'spmc', '') | |
| 608 | + this.$set(row, 'sptm', '') | |
| 609 | + this.$set(row, 'dw', '') | |
| 610 | + this.$set(row, 'sl', undefined) | |
| 611 | + this.$set(row, 'dj', undefined) | |
| 612 | + this.$set(row, 'je', undefined) | |
| 613 | + this.$set(row, 'description', '') | |
| 614 | + this.$set(row, 'spxlhLoaded', false) | |
| 615 | + this.$set(row, 'xlhList', []) | |
| 616 | + return | |
| 617 | + } | |
| 606 | 618 | const product = this.spbhOptions.find(item => item.F_Id === row.spbh); |
| 607 | 619 | console.log('选中商品', row.spbh, product); |
| 608 | 620 | if (product) { | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtYsthd/detail-view.vue
| ... | ... | @@ -162,7 +162,7 @@ |
| 162 | 162 | <el-descriptions-item label="退款账户"> |
| 163 | 163 | <span class="cell-nowrap"> |
| 164 | 164 | <i class="el-icon-bank-card desc-icon desc-icon--success" /> |
| 165 | - {{ labelFromOptions(detail.skzh, skzhOptions) }} | |
| 165 | + {{ displaySkzh() }} | |
| 166 | 166 | </span> |
| 167 | 167 | </el-descriptions-item> |
| 168 | 168 | <el-descriptions-item label="审核人"> |
| ... | ... | @@ -205,6 +205,7 @@ import { previewDataInterface } from '@/api/systemData/dataInterface' |
| 205 | 205 | import { dynamicText } from '@/filters' |
| 206 | 206 | import YsckdDetailView from '../wtYsckd/detail-view' |
| 207 | 207 | import { getAccountSelector } from '@/api/extend/wtAccount' |
| 208 | +import { formatWtSkzhDisplay } from '@/utils/wtComboSkzhDisplay' | |
| 208 | 209 | |
| 209 | 210 | export default { |
| 210 | 211 | name: 'WtYsthdDetailView', |
| ... | ... | @@ -267,6 +268,10 @@ export default { |
| 267 | 268 | if (this.$refs.YsckdDetailView) this.$refs.YsckdDetailView.init(id) |
| 268 | 269 | }) |
| 269 | 270 | }, |
| 271 | + displaySkzh() { | |
| 272 | + if (!this.detail) return '无' | |
| 273 | + return formatWtSkzhDisplay(this.detail, this.skzhOptions, { mode: 'refund' }) || '无' | |
| 274 | + }, | |
| 270 | 275 | labelFromOptions(value, options) { |
| 271 | 276 | if (value === null || value === undefined || value === '') return '无' |
| 272 | 277 | const opts = options || [] | ... | ... |
Antis.Erp.Plat/antis-ncc-admin/src/views/wtYsthd/index.vue
| ... | ... | @@ -94,8 +94,8 @@ |
| 94 | 94 | <screenfull isContainer /> |
| 95 | 95 | </div> |
| 96 | 96 | </div> |
| 97 | - <NCC-table v-loading="listLoading" :data="list" has-c @selection-change="handleSelectionChange"> | |
| 98 | - <el-table-column prop="id" label="单据编号" align="left" /> | |
| 97 | + <NCC-table v-loading="listLoading" class="wt-ysthd-table" :data="list" has-c @selection-change="handleSelectionChange"> | |
| 98 | + <el-table-column prop="id" label="单据编号" align="left" min-width="150" show-overflow-tooltip /> | |
| 99 | 99 | <el-table-column label="预售出库单号" align="left" min-width="150" show-overflow-tooltip> |
| 100 | 100 | <template slot-scope="scope"> |
| 101 | 101 | <span class="cell-nowrap" @click.stop> |
| ... | ... | @@ -110,29 +110,27 @@ |
| 110 | 110 | </span> |
| 111 | 111 | </template> |
| 112 | 112 | </el-table-column> |
| 113 | - <el-table-column prop="djrq" label="单据日期" align="left" :formatter="ncc.tableDateFormat" /> | |
| 114 | - <el-table-column label="入库仓库" prop="cjck" align="left"> | |
| 113 | + <el-table-column prop="djrq" label="单据日期" align="left" min-width="110" :formatter="ncc.tableDateFormat" show-overflow-tooltip /> | |
| 114 | + <el-table-column label="入库仓库" prop="cjck" align="left" min-width="120" show-overflow-tooltip> | |
| 115 | 115 | <template slot-scope="scope">{{ formatListCjck(scope.row) }}</template> |
| 116 | 116 | </el-table-column> |
| 117 | - <el-table-column prop="jsr" label="经手人" align="left" /> | |
| 118 | - <!-- <el-table-column prop="ysje" label="优惠金额" align="left" /> --> | |
| 119 | - <el-table-column label="退款账户" prop="skzh" align="left"> | |
| 120 | - <template slot-scope="scope">{{ scope.row.skzh | dynamicText(skzhOptions) }}</template> | |
| 117 | + <el-table-column prop="jsr" label="经手人" align="left" min-width="88" show-overflow-tooltip /> | |
| 118 | + <el-table-column label="退款账户" prop="skzh" align="left" min-width="220" show-overflow-tooltip> | |
| 119 | + <template slot-scope="scope">{{ formatSkzhRow(scope.row) }}</template> | |
| 121 | 120 | </el-table-column> |
| 122 | - <el-table-column prop="skje" label="退款金额" align="left" /> | |
| 123 | - <!-- <el-table-column prop="zdr" label="制单人" align="left" /> --> | |
| 124 | - <el-table-column prop="shr" label="审核人" align="left" /> | |
| 125 | - <el-table-column prop="gzr" label="过账人" align="left" /> | |
| 126 | - <el-table-column prop="bz" label="备注" align="left" /> | |
| 127 | - <el-table-column prop="djlx" label="单据类型" align="left" /> | |
| 128 | - <el-table-column prop="djzt" label="审核状态" align="left"> | |
| 121 | + <el-table-column prop="skje" label="退款金额" align="right" min-width="100" show-overflow-tooltip /> | |
| 122 | + <el-table-column prop="shr" label="审核人" align="left" min-width="88" show-overflow-tooltip /> | |
| 123 | + <el-table-column prop="gzr" label="过账人" align="left" min-width="88" show-overflow-tooltip /> | |
| 124 | + <el-table-column prop="bz" label="备注" align="left" min-width="160" show-overflow-tooltip /> | |
| 125 | + <el-table-column prop="djlx" label="单据类型" align="left" min-width="110" show-overflow-tooltip /> | |
| 126 | + <el-table-column prop="djzt" label="审核状态" align="left" min-width="110" show-overflow-tooltip> | |
| 129 | 127 | <template slot-scope="scope"> |
| 130 | 128 | <el-tag v-if="scope.row.djzt === '已审核'" type="success">已审核</el-tag> |
| 131 | 129 | <el-tag v-else-if="scope.row.djzt === '一级已审'" type="">一级已审</el-tag> |
| 132 | 130 | <el-tag v-else type="warning">{{ scope.row.djzt || '待审核' }}</el-tag> |
| 133 | 131 | </template> |
| 134 | 132 | </el-table-column> |
| 135 | - <el-table-column label="摘要" align="left" min-width="200" show-overflow-tooltip class-name="cell-nowrap"> | |
| 133 | + <el-table-column label="摘要" align="left" min-width="200" show-overflow-tooltip> | |
| 136 | 134 | <template slot-scope="scope"> |
| 137 | 135 | <ncc-table-summary-cell :row="scope.row" fields="zy,Zy" /> |
| 138 | 136 | </template> |
| ... | ... | @@ -166,6 +164,7 @@ |
| 166 | 164 | import { previewDataInterface } from '@/api/systemData/dataInterface' |
| 167 | 165 | import { promptApprovalRemark, postApproveGeneric } from '@/utils/wtRejectApproval' |
| 168 | 166 | import { getAccountSelector } from '@/api/extend/wtAccount' |
| 167 | + import { formatWtSkzhDisplay } from '@/utils/wtComboSkzhDisplay' | |
| 169 | 168 | export default { |
| 170 | 169 | components: { NCCForm, DetailView, YsckdDetailView, ExportBox }, |
| 171 | 170 | data() { |
| ... | ... | @@ -283,6 +282,9 @@ |
| 283 | 282 | this.skzhOptions = res.data.list |
| 284 | 283 | }); |
| 285 | 284 | }, |
| 285 | + formatSkzhRow(row) { | |
| 286 | + return formatWtSkzhDisplay(row, this.skzhOptions, { mode: 'refund' }) | |
| 287 | + }, | |
| 286 | 288 | initData() { |
| 287 | 289 | this.listLoading = true; |
| 288 | 290 | let _query = { |
| ... | ... | @@ -459,4 +461,9 @@ |
| 459 | 461 | .ycddh-link { |
| 460 | 462 | font-weight: normal; |
| 461 | 463 | } |
| 464 | +.wt-ysthd-table { | |
| 465 | + ::v-deep .el-table .cell { | |
| 466 | + white-space: nowrap; | |
| 467 | + } | |
| 468 | +} | |
| 462 | 469 | </style> |
| 463 | 470 | \ No newline at end of file | ... | ... |
Antis.Erp.Plat/douyin/DouyinLogistics.API/Controllers/OrdersController.cs
| ... | ... | @@ -73,6 +73,48 @@ public class OrdersController : ControllerBase |
| 73 | 73 | } |
| 74 | 74 | |
| 75 | 75 | /// <summary> |
| 76 | + /// 拆分合并单:把这组订单从"同买家+同地址"的自动合并组中拆出来,分别单独发货。 | |
| 77 | + /// 入参 orderIds 传当前合并行里的所有子订单 id(即列表返回的 mergedOrderIds)。 | |
| 78 | + /// 已发货/已取消/已退款的订单会被忽略(只处理待发货的)。 | |
| 79 | + /// </summary> | |
| 80 | + [HttpPost("split")] | |
| 81 | + public async Task<ActionResult> SplitMergedOrders([FromBody] SplitOrdersRequest request) | |
| 82 | + { | |
| 83 | + if (request?.OrderIds == null || request.OrderIds.Count == 0) | |
| 84 | + return BadRequest(new { message = "请传入要拆分的订单 id 列表(orderIds)" }); | |
| 85 | + try | |
| 86 | + { | |
| 87 | + var updated = await _orderService.SplitMergedOrdersAsync(request.OrderIds); | |
| 88 | + return Ok(new { message = $"已拆分 {updated} 笔订单", updated }); | |
| 89 | + } | |
| 90 | + catch (Exception ex) | |
| 91 | + { | |
| 92 | + _logger.LogError(ex, "拆分合并单失败: {Ids}", string.Join(",", request.OrderIds)); | |
| 93 | + return StatusCode(500, new { message = "拆分合并单失败", error = ex.Message }); | |
| 94 | + } | |
| 95 | + } | |
| 96 | + | |
| 97 | + /// <summary> | |
| 98 | + /// 恢复合并:取消"拆分"标记,下次列表会按同买家+同地址重新自动合并。 | |
| 99 | + /// </summary> | |
| 100 | + [HttpPost("unsplit")] | |
| 101 | + public async Task<ActionResult> UnsplitOrders([FromBody] SplitOrdersRequest request) | |
| 102 | + { | |
| 103 | + if (request?.OrderIds == null || request.OrderIds.Count == 0) | |
| 104 | + return BadRequest(new { message = "请传入要恢复合并的订单 id 列表(orderIds)" }); | |
| 105 | + try | |
| 106 | + { | |
| 107 | + var updated = await _orderService.UnsplitOrdersAsync(request.OrderIds); | |
| 108 | + return Ok(new { message = $"已恢复 {updated} 笔订单", updated }); | |
| 109 | + } | |
| 110 | + catch (Exception ex) | |
| 111 | + { | |
| 112 | + _logger.LogError(ex, "恢复合并单失败: {Ids}", string.Join(",", request.OrderIds)); | |
| 113 | + return StatusCode(500, new { message = "恢复合并单失败", error = ex.Message }); | |
| 114 | + } | |
| 115 | + } | |
| 116 | + | |
| 117 | + /// <summary> | |
| 76 | 118 | /// 诊断订单合并情况(排查为何未合并) |
| 77 | 119 | /// </summary> |
| 78 | 120 | [HttpGet("debug/merge")] |
| ... | ... | @@ -233,6 +275,16 @@ public class OrdersController : ControllerBase |
| 233 | 275 | product_pic = itemObj["product_pic"]?.ToString() |
| 234 | 276 | ?? itemObj["ProductPic"]?.ToString() |
| 235 | 277 | ?? "", |
| 278 | + // 抖音商品图(商品明细区展示);老发货单 JSON 里没有该字段,兜底用 product_pic | |
| 279 | + douyin_pic = itemObj["douyin_pic"]?.ToString() | |
| 280 | + ?? itemObj["DouyinPic"]?.ToString() | |
| 281 | + ?? itemObj["product_pic"]?.ToString() | |
| 282 | + ?? itemObj["ProductPic"]?.ToString() | |
| 283 | + ?? "", | |
| 284 | + // ERP 商品图(商品清单区展示);老发货单 JSON 里没有时为空,前端会在加载后按 spbm 再拉一次 | |
| 285 | + erp_pic = itemObj["erp_pic"]?.ToString() | |
| 286 | + ?? itemObj["ErpPic"]?.ToString() | |
| 287 | + ?? "", | |
| 236 | 288 | item_num = itemObj["item_num"]?.ToObject<int>() |
| 237 | 289 | ?? itemObj["ItemNum"]?.ToObject<int>() |
| 238 | 290 | ?? itemObj["itemNum"]?.ToObject<int>() |
| ... | ... | @@ -266,7 +318,14 @@ public class OrdersController : ControllerBase |
| 266 | 318 | ?? false, |
| 267 | 319 | isFromErp = itemObj["isFromErp"]?.ToObject<bool>() |
| 268 | 320 | ?? itemObj["IsFromErp"]?.ToObject<bool>() |
| 269 | - ?? false | |
| 321 | + ?? false, | |
| 322 | + // 已选序列号:编辑发货单时需要回显,避免用户重新打开弹窗时历史选择丢失 | |
| 323 | + selectedSerialNumbers = (itemObj["selectedSerialNumbers"] | |
| 324 | + ?? itemObj["SelectedSerialNumbers"] | |
| 325 | + ?? itemObj["serialNumbers"]) | |
| 326 | + is Newtonsoft.Json.Linq.JArray snArr | |
| 327 | + ? snArr.Select(x => x?.ToString() ?? "").Where(s => !string.IsNullOrEmpty(s)).ToList() | |
| 328 | + : new List<string>() | |
| 270 | 329 | }; |
| 271 | 330 | |
| 272 | 331 | // 记录ERP商品的调试信息 |
| ... | ... | @@ -323,7 +382,14 @@ public class OrdersController : ControllerBase |
| 323 | 382 | waybill = waybill != null ? new |
| 324 | 383 | { |
| 325 | 384 | cjck = waybill.Cjck, |
| 326 | - status = waybill.Status | |
| 385 | + status = waybill.Status, | |
| 386 | + // 已发货时前端要回显物流单号/物流公司 | |
| 387 | + trackingNumber = waybill.TrackingNumber ?? "", | |
| 388 | + logisticsCompany = waybill.LogisticsCompany ?? "", | |
| 389 | + shipTime = waybill.ShipTime, | |
| 390 | + // 商家实际收入(元);null 表示还没改过,前端默认等于用户支付金额 | |
| 391 | + merchantIncome = waybill.MerchantIncome, | |
| 392 | + fhr = waybill.Fhr ?? "" | |
| 327 | 393 | } : null, |
| 328 | 394 | productItems = productItems |
| 329 | 395 | }); |
| ... | ... | @@ -383,6 +449,14 @@ public class OrdersController : ControllerBase |
| 383 | 449 | { |
| 384 | 450 | product_name = item["product_name"]?.ToString(), |
| 385 | 451 | product_pic = item["product_pic"]?.ToString(), |
| 452 | + // 抖音图/ERP 图拆两个字段;老数据没有 douyin_pic 时兜底用 product_pic | |
| 453 | + douyin_pic = item["douyin_pic"]?.ToString() | |
| 454 | + ?? item["DouyinPic"]?.ToString() | |
| 455 | + ?? item["product_pic"]?.ToString() | |
| 456 | + ?? "", | |
| 457 | + erp_pic = item["erp_pic"]?.ToString() | |
| 458 | + ?? item["ErpPic"]?.ToString() | |
| 459 | + ?? "", | |
| 386 | 460 | item_num = item["item_num"]?.ToObject<int>() ?? item["ItemNum"]?.ToObject<int>() ?? 1, |
| 387 | 461 | goods_price = item["goods_price"]?.ToObject<long>() ?? item["GoodsPrice"]?.ToObject<long>() ?? 0, |
| 388 | 462 | spec = item["spec"], |
| ... | ... | @@ -398,6 +472,21 @@ public class OrdersController : ControllerBase |
| 398 | 472 | var orderIdsStr = string.Join(",", orders.Select(o => o.OrderId)); |
| 399 | 473 | var allBuyerWords = string.Join("\n", orders.Where(o => !string.IsNullOrEmpty(o.BuyerWords)).Select(o => o.BuyerWords)); |
| 400 | 474 | var allSellerWords = string.Join("\n", orders.Where(o => !string.IsNullOrEmpty(o.SellerWords)).Select(o => o.SellerWords)); |
| 475 | + | |
| 476 | + // 合并订单也可能已经存在发货单(任意一条子订单的 id 都可能被当作 waybill.OrderId),编辑时需要回显已保存的商家实际收入/物流单号等 | |
| 477 | + Waybill? mergedWaybill = null; | |
| 478 | + try | |
| 479 | + { | |
| 480 | + mergedWaybill = await db.Queryable<Waybill>() | |
| 481 | + .Where(w => idList.Contains(w.OrderId)) | |
| 482 | + .OrderByDescending(w => w.CreateTime) | |
| 483 | + .FirstAsync(); | |
| 484 | + } | |
| 485 | + catch (Exception exw) | |
| 486 | + { | |
| 487 | + _logger.LogWarning(exw, "查询合并订单发货单失败: ids={Ids}", ids); | |
| 488 | + } | |
| 489 | + | |
| 401 | 490 | return Ok(new |
| 402 | 491 | { |
| 403 | 492 | order = new |
| ... | ... | @@ -420,7 +509,16 @@ public class OrdersController : ControllerBase |
| 420 | 509 | buyerWords = allBuyerWords, |
| 421 | 510 | sellerWords = allSellerWords |
| 422 | 511 | }, |
| 423 | - waybill = (object?)null, | |
| 512 | + waybill = mergedWaybill != null ? (object)new | |
| 513 | + { | |
| 514 | + cjck = mergedWaybill.Cjck, | |
| 515 | + status = mergedWaybill.Status, | |
| 516 | + trackingNumber = mergedWaybill.TrackingNumber ?? "", | |
| 517 | + logisticsCompany = mergedWaybill.LogisticsCompany ?? "", | |
| 518 | + shipTime = mergedWaybill.ShipTime, | |
| 519 | + merchantIncome = mergedWaybill.MerchantIncome, | |
| 520 | + fhr = mergedWaybill.Fhr ?? "" | |
| 521 | + } : null, | |
| 424 | 522 | productItems |
| 425 | 523 | }); |
| 426 | 524 | } |
| ... | ... | @@ -461,6 +559,10 @@ public class OrdersController : ControllerBase |
| 461 | 559 | { |
| 462 | 560 | try |
| 463 | 561 | { |
| 562 | + // 每次同步前先从 ERP 刷新一次店铺配置,保证 ERP 里的改动无需重启抖音服务即可生效 | |
| 563 | + try { await _douyinFactory.ReloadAsync(); } | |
| 564 | + catch (Exception reloadEx) { _logger.LogWarning(reloadEx, "同步前刷新店铺配置失败,继续使用当前已加载的配置"); } | |
| 565 | + | |
| 464 | 566 | await _orderService.SyncOrdersAsync(shopId); |
| 465 | 567 | return Ok(new { message = "订单同步成功" }); |
| 466 | 568 | } |
| ... | ... | @@ -1140,17 +1242,23 @@ public class OrdersController : ControllerBase |
| 1140 | 1242 | { |
| 1141 | 1243 | if (item is Newtonsoft.Json.Linq.JObject itemObj) |
| 1142 | 1244 | { |
| 1143 | - // 将productCode字段更新为转换后的商品ID(F_Id) | |
| 1144 | 1245 | itemObj["productCode"] = actualProductCode; |
| 1145 | - _logger.LogInformation("更新序列号数据中的productCode: {OriginalProductCode} -> {ActualProductCode}", | |
| 1146 | - productCode, actualProductCode); | |
| 1147 | 1246 | } |
| 1148 | 1247 | } |
| 1149 | 1248 | } |
| 1150 | - | |
| 1151 | - return Ok(new { code = 200, serialNumbers = serialNumbers }); | |
| 1249 | + | |
| 1250 | + // 注意:这里不能用 Ok(new { code = 200, serialNumbers = serialNumbers }) | |
| 1251 | + // ASP.NET Core 默认用 System.Text.Json,会把 Newtonsoft 的 JObject 当作 | |
| 1252 | + // IEnumerable<JProperty> 递归序列化,结果变成 [[[]],[[]],...] 多层嵌套空数组。 | |
| 1253 | + // 改用 Newtonsoft.Json 直接序列化后原样写回,保证对象字段完整。 | |
| 1254 | + var payload = JsonConvert.SerializeObject(new | |
| 1255 | + { | |
| 1256 | + code = 200, | |
| 1257 | + serialNumbers = serialNumbersArray ?? new Newtonsoft.Json.Linq.JArray() | |
| 1258 | + }); | |
| 1259 | + return Content(payload, "application/json; charset=utf-8"); | |
| 1152 | 1260 | } |
| 1153 | - | |
| 1261 | + | |
| 1154 | 1262 | // 如果没有找到序列号,返回空列表 |
| 1155 | 1263 | return Ok(new { code = 200, serialNumbers = new List<object>() }); |
| 1156 | 1264 | } |
| ... | ... | @@ -1292,6 +1400,7 @@ public class OrdersController : ControllerBase |
| 1292 | 1400 | shopName = data["shopName"]?.ToString() ?? "", |
| 1293 | 1401 | kh = data["kh"]?.ToString() ?? "", |
| 1294 | 1402 | skzh = data["skzh"]?.ToString() ?? "", |
| 1403 | + ck = data["ck"]?.ToString() ?? "", | |
| 1295 | 1404 | bz = data["bz"]?.ToString() ?? "" |
| 1296 | 1405 | } |
| 1297 | 1406 | }); |
| ... | ... | @@ -1532,7 +1641,7 @@ public class OrdersController : ControllerBase |
| 1532 | 1641 | } |
| 1533 | 1642 | |
| 1534 | 1643 | /// <summary> |
| 1535 | - /// 获取收款账户列表(代理 ERP WtSkzhb 接口) | |
| 1644 | + /// 获取收款账户列表(代理 ERP WtAccount 接口,wt_account 是账户主表) | |
| 1536 | 1645 | /// </summary> |
| 1537 | 1646 | [HttpGet("payment-accounts")] |
| 1538 | 1647 | public async Task<ActionResult> GetPaymentAccounts() |
| ... | ... | @@ -1543,7 +1652,7 @@ public class OrdersController : ControllerBase |
| 1543 | 1652 | var httpClientFactory = HttpContext.RequestServices.GetRequiredService<IHttpClientFactory>(); |
| 1544 | 1653 | var httpClient = httpClientFactory.CreateClient(); |
| 1545 | 1654 | |
| 1546 | - var apiUrl = $"{erpApiConfig.BaseUrl}/api/Extend/WtSkzhb?currentPage=1&pageSize=1000"; | |
| 1655 | + var apiUrl = $"{erpApiConfig.BaseUrl}/api/Extend/WtAccount?currentPage=1&pageSize=1000&status=1"; | |
| 1547 | 1656 | var request = await _orderService.CreateAuthenticatedRequestAsync(System.Net.Http.HttpMethod.Get, apiUrl); |
| 1548 | 1657 | var response = await httpClient.SendAsync(request); |
| 1549 | 1658 | if (!response.IsSuccessStatusCode) |
| ... | ... | @@ -1558,8 +1667,17 @@ public class OrdersController : ControllerBase |
| 1558 | 1667 | if (result?["data"] is JObject dataObj) |
| 1559 | 1668 | { |
| 1560 | 1669 | var list = dataObj["list"] as JArray ?? new JArray(); |
| 1561 | - var accountList = list.Select(item => item is JObject o ? o.ToObject<Dictionary<string, object>>() : null) | |
| 1562 | - .Where(x => x != null).ToList(); | |
| 1670 | + var accountList = list.Select(item => | |
| 1671 | + { | |
| 1672 | + if (item is not JObject o) return null; | |
| 1673 | + return (object)new | |
| 1674 | + { | |
| 1675 | + id = o["id"]?.ToString() ?? "", | |
| 1676 | + accountName = o["accountName"]?.ToString() ?? "", | |
| 1677 | + accountCode = o["accountCode"]?.ToString() ?? "", | |
| 1678 | + category = o["category"]?.ToString() ?? "" | |
| 1679 | + }; | |
| 1680 | + }).Where(x => x != null).ToList(); | |
| 1563 | 1681 | return Ok(new { code = 200, data = accountList }); |
| 1564 | 1682 | } |
| 1565 | 1683 | return Ok(new { code = 200, data = new List<object>() }); |
| ... | ... | @@ -1572,6 +1690,62 @@ public class OrdersController : ControllerBase |
| 1572 | 1690 | } |
| 1573 | 1691 | |
| 1574 | 1692 | /// <summary> |
| 1693 | + /// 获取 ERP 用户列表(用于发货单中"发货人"下拉),代理 ERP /api/permission/Users/All | |
| 1694 | + /// </summary> | |
| 1695 | + [HttpGet("users")] | |
| 1696 | + public async Task<ActionResult> GetUsers() | |
| 1697 | + { | |
| 1698 | + try | |
| 1699 | + { | |
| 1700 | + var erpApiConfig = HttpContext.RequestServices.GetRequiredService<ErpApiConfig>(); | |
| 1701 | + var httpClientFactory = HttpContext.RequestServices.GetRequiredService<IHttpClientFactory>(); | |
| 1702 | + var httpClient = httpClientFactory.CreateClient(); | |
| 1703 | + | |
| 1704 | + var apiUrl = $"{erpApiConfig.BaseUrl}/api/permission/Users/All"; | |
| 1705 | + var request = await _orderService.CreateAuthenticatedRequestAsync(System.Net.Http.HttpMethod.Get, apiUrl); | |
| 1706 | + var response = await httpClient.SendAsync(request); | |
| 1707 | + if (!response.IsSuccessStatusCode) | |
| 1708 | + { | |
| 1709 | + var errBody = await response.Content.ReadAsStringAsync(); | |
| 1710 | + _logger.LogWarning("ERP 用户列表接口返回 {StatusCode}: {Body}", response.StatusCode, errBody); | |
| 1711 | + return StatusCode(500, new { message = "查询用户列表失败" }); | |
| 1712 | + } | |
| 1713 | + | |
| 1714 | + var content = await response.Content.ReadAsStringAsync(); | |
| 1715 | + var result = JsonConvert.DeserializeObject<JObject>(content); | |
| 1716 | + // NCC 框架返回 { code, msg, data: [...] },其中 data 可能是数组或 { list: [...] } | |
| 1717 | + JArray list = null; | |
| 1718 | + var dataToken = result?["data"]; | |
| 1719 | + if (dataToken is JArray arr) list = arr; | |
| 1720 | + else if (dataToken is JObject obj && obj["list"] is JArray subArr) list = subArr; | |
| 1721 | + list ??= new JArray(); | |
| 1722 | + | |
| 1723 | + var userList = list.Select(item => | |
| 1724 | + { | |
| 1725 | + if (item is not JObject o) return null; | |
| 1726 | + // ERP 用户 Id 字段:兼容 id / F_Id | |
| 1727 | + var id = o["id"]?.ToString() ?? o["F_Id"]?.ToString() ?? o["userId"]?.ToString() ?? ""; | |
| 1728 | + var realName = o["realName"]?.ToString() ?? o["F_RealName"]?.ToString() ?? ""; | |
| 1729 | + var account = o["account"]?.ToString() ?? o["F_Account"]?.ToString() ?? ""; | |
| 1730 | + if (string.IsNullOrEmpty(id)) return null; | |
| 1731 | + return (object)new | |
| 1732 | + { | |
| 1733 | + id, | |
| 1734 | + realName, | |
| 1735 | + account, | |
| 1736 | + displayName = !string.IsNullOrEmpty(realName) ? realName : (account ?? id) | |
| 1737 | + }; | |
| 1738 | + }).Where(x => x != null).ToList(); | |
| 1739 | + return Ok(new { code = 200, data = userList }); | |
| 1740 | + } | |
| 1741 | + catch (Exception ex) | |
| 1742 | + { | |
| 1743 | + _logger.LogError(ex, "查询用户列表失败"); | |
| 1744 | + return StatusCode(500, new { message = "查询用户列表失败", error = ex.Message }); | |
| 1745 | + } | |
| 1746 | + } | |
| 1747 | + | |
| 1748 | + /// <summary> | |
| 1575 | 1749 | /// 调试接口:查看发货单的原始ProductInfo数据 |
| 1576 | 1750 | /// </summary> |
| 1577 | 1751 | [HttpGet("debug/waybill-productinfo/{orderId}")] |
| ... | ... | @@ -1952,6 +2126,35 @@ public class OrdersController : ControllerBase |
| 1952 | 2126 | }); |
| 1953 | 2127 | return Ok(new { data = shops }); |
| 1954 | 2128 | } |
| 2129 | + | |
| 2130 | + /// <summary> | |
| 2131 | + /// 从 ERP(WtDyDpsz/internal/all)重新拉取抖音店铺配置并热替换。 | |
| 2132 | + /// 在 ERP「抖音店铺设置」中新增/修改/停用店铺后调用,无需重启抖音服务。 | |
| 2133 | + /// </summary> | |
| 2134 | + [HttpPost("/api/shop-configs/reload")] | |
| 2135 | + public async Task<ActionResult> ReloadShopConfigs() | |
| 2136 | + { | |
| 2137 | + try | |
| 2138 | + { | |
| 2139 | + var count = await _douyinFactory.ReloadAsync(); | |
| 2140 | + var shops = _douyinFactory.GetAllConfigs().Select(c => new | |
| 2141 | + { | |
| 2142 | + shopId = c.ShopId, | |
| 2143 | + shopName = c.ShopName | |
| 2144 | + }); | |
| 2145 | + return Ok(new | |
| 2146 | + { | |
| 2147 | + message = $"已从 ERP 加载 {count} 个抖音店铺", | |
| 2148 | + count, | |
| 2149 | + data = shops | |
| 2150 | + }); | |
| 2151 | + } | |
| 2152 | + catch (Exception ex) | |
| 2153 | + { | |
| 2154 | + _logger.LogError(ex, "刷新抖音店铺配置失败"); | |
| 2155 | + return StatusCode(500, new { message = "刷新抖音店铺配置失败", error = ex.Message }); | |
| 2156 | + } | |
| 2157 | + } | |
| 1955 | 2158 | } |
| 1956 | 2159 | |
| 1957 | 2160 | public class UpdateSellerRemarkRequest |
| ... | ... | @@ -1959,6 +2162,14 @@ public class UpdateSellerRemarkRequest |
| 1959 | 2162 | public string? SellerWords { get; set; } |
| 1960 | 2163 | } |
| 1961 | 2164 | |
| 2165 | +/// <summary> | |
| 2166 | +/// 拆分 / 恢复合并单入参:orderIds 一般来自列表行的 mergedOrderIds。 | |
| 2167 | +/// </summary> | |
| 2168 | +public class SplitOrdersRequest | |
| 2169 | +{ | |
| 2170 | + public List<int> OrderIds { get; set; } = new List<int>(); | |
| 2171 | +} | |
| 2172 | + | |
| 1962 | 2173 | public class ManualShipRequest |
| 1963 | 2174 | { |
| 1964 | 2175 | /// <summary>运单号</summary> | ... | ... |
Antis.Erp.Plat/douyin/DouyinLogistics.API/Controllers/WaybillController.cs
| ... | ... | @@ -180,6 +180,12 @@ public class WaybillController : ControllerBase |
| 180 | 180 | return BadRequest(new { message = "已取消、已退款或退款中的订单不允许进行任何操作" }); |
| 181 | 181 | } |
| 182 | 182 | |
| 183 | + // 发货人必填(写入 ERP 销售出库单 fhr) | |
| 184 | + if (string.IsNullOrWhiteSpace(request.Fhr)) | |
| 185 | + { | |
| 186 | + return BadRequest(new { message = "请先选择发货人" }); | |
| 187 | + } | |
| 188 | + | |
| 183 | 189 | var db = HttpContext.RequestServices.GetRequiredService<ISqlSugarClient>(); |
| 184 | 190 | |
| 185 | 191 | // 检查是否已存在发货单(按订单ID查找最新的发货单) |
| ... | ... | @@ -259,13 +265,30 @@ public class WaybillController : ControllerBase |
| 259 | 265 | _logger.LogWarning("更新发货单商品信息 - ProductItems为空或数量为0: OrderId={OrderId}", request.OrderId); |
| 260 | 266 | } |
| 261 | 267 | existingWaybill.Remark = request.Remark ?? existingWaybill.Remark; |
| 268 | + // 商家实际收入:前端每次提交都会带最新值;为空则保留原值 | |
| 269 | + if (request.MerchantIncome.HasValue) | |
| 270 | + existingWaybill.MerchantIncome = request.MerchantIncome.Value; | |
| 271 | + // 发货人:已在入口处校验非空 | |
| 272 | + existingWaybill.Fhr = request.Fhr; | |
| 262 | 273 | existingWaybill.UpdateTime = DateTime.Now; |
| 263 | 274 | |
| 264 | - // 商品明细变更时清除已关联的销售出库单,让后续流程重新创建 | |
| 275 | + // 发货单被重新编辑:先到 ERP 删掉旧的销售出库单,再清除本地关联,让后续流程生成新单 | |
| 265 | 276 | if (!string.IsNullOrEmpty(existingWaybill.SalesOrderId)) |
| 266 | 277 | { |
| 267 | - _logger.LogInformation("发货单商品明细已更新,清除旧销售出库单关联: WaybillId={WaybillId}, OldSalesOrderId={OldSalesOrderId}", | |
| 268 | - existingWaybill.Id, existingWaybill.SalesOrderId); | |
| 278 | + var oldSalesOrderId = existingWaybill.SalesOrderId; | |
| 279 | + _logger.LogInformation("发货单被重新编辑,准备删除 ERP 旧销售出库单: WaybillId={WaybillId}, OldSalesOrderId={OldSalesOrderId}", | |
| 280 | + existingWaybill.Id, oldSalesOrderId); | |
| 281 | + | |
| 282 | + var deleted = await _orderService.DeleteErpSalesOrderAsync(oldSalesOrderId); | |
| 283 | + if (!deleted) | |
| 284 | + { | |
| 285 | + // 旧单删不掉(通常是已被审核或已关联退货单),不能继续创建新单,否则 ERP 会出现重复出库 | |
| 286 | + return BadRequest(new | |
| 287 | + { | |
| 288 | + message = $"无法更新发货单:旧的销售出库单({oldSalesOrderId})在 ERP 中删除失败,可能已被审核或已关联退货单,请先到 ERP 处理后再试。" | |
| 289 | + }); | |
| 290 | + } | |
| 291 | + | |
| 269 | 292 | existingWaybill.SalesOrderId = null; |
| 270 | 293 | } |
| 271 | 294 | |
| ... | ... | @@ -273,6 +296,7 @@ public class WaybillController : ControllerBase |
| 273 | 296 | waybill = existingWaybill; |
| 274 | 297 | waybillId = existingWaybill.Id; |
| 275 | 298 | isUpdate = true; |
| 299 | + DouyinLogistics.API.Services.OrderService.InvalidateOrderListCache(); | |
| 276 | 300 | |
| 277 | 301 | _logger.LogInformation("更新发货单成功: WaybillId={WaybillId}, OrderId={OrderId}", waybillId, request.OrderId); |
| 278 | 302 | } |
| ... | ... | @@ -300,6 +324,8 @@ public class WaybillController : ControllerBase |
| 300 | 324 | Cjck = request.Cjck, |
| 301 | 325 | ProductInfo = "", |
| 302 | 326 | Remark = request.Remark, |
| 327 | + MerchantIncome = request.MerchantIncome, | |
| 328 | + Fhr = request.Fhr, | |
| 303 | 329 | CreateTime = DateTime.Now, |
| 304 | 330 | UpdateTime = DateTime.Now |
| 305 | 331 | }; |
| ... | ... | @@ -337,6 +363,7 @@ public class WaybillController : ControllerBase |
| 337 | 363 | } |
| 338 | 364 | |
| 339 | 365 | waybillId = await db.Insertable(waybill).ExecuteReturnIdentityAsync(); |
| 366 | + DouyinLogistics.API.Services.OrderService.InvalidateOrderListCache(); | |
| 340 | 367 | _logger.LogInformation("手动创建发货单成功: WaybillId={WaybillId}, OrderId={OrderId}", waybillId, request.OrderId); |
| 341 | 368 | } |
| 342 | 369 | |
| ... | ... | @@ -349,7 +376,8 @@ public class WaybillController : ControllerBase |
| 349 | 376 | request.OrderId, request.DouyinOrderIds); |
| 350 | 377 | var salesOrderResult = await _orderService.CreateSalesOrderAsync( |
| 351 | 378 | request.OrderId, request.Kh, request.Skzh, request.Djlx, |
| 352 | - request.DouyinOrderIds, request.MergedOrderInternalIds); | |
| 379 | + request.DouyinOrderIds, request.MergedOrderInternalIds, | |
| 380 | + request.MerchantIncome, request.Fhr); | |
| 353 | 381 | |
| 354 | 382 | if (salesOrderResult.Success) |
| 355 | 383 | { |
| ... | ... | @@ -816,5 +844,13 @@ public class ManualCreateWaybillRequest |
| 816 | 844 | public List<int>? MergedOrderInternalIds { get; set; } |
| 817 | 845 | public List<object>? ProductItems { get; set; } |
| 818 | 846 | public string? Remark { get; set; } |
| 847 | + /// <summary> | |
| 848 | + /// 商家实际收入(元)。前端可修改,默认 = 用户支付金额;最终写入 ERP 销售出库单的 skje。 | |
| 849 | + /// </summary> | |
| 850 | + public decimal? MerchantIncome { get; set; } | |
| 851 | + /// <summary> | |
| 852 | + /// 发货人(ERP 用户 Id),必填。写入 ERP 销售出库单的 fhr 字段。 | |
| 853 | + /// </summary> | |
| 854 | + public string? Fhr { get; set; } | |
| 819 | 855 | } |
| 820 | 856 | ... | ... |
Antis.Erp.Plat/douyin/DouyinLogistics.API/Models/Order.cs
| ... | ... | @@ -195,6 +195,12 @@ public class Order |
| 195 | 195 | public string? OpenId { get; set; } |
| 196 | 196 | |
| 197 | 197 | /// <summary> |
| 198 | + /// 是否已从合并组中拆出单独发货。true 表示用户手动拆分,列表不再按同买家+同地址合并此单。 | |
| 199 | + /// </summary> | |
| 200 | + [SugarColumn(ColumnDataType = "tinyint", IsNullable = false, DefaultValue = "0")] | |
| 201 | + public bool NoMerge { get; set; } | |
| 202 | + | |
| 203 | + /// <summary> | |
| 198 | 204 | /// 合并的订单ID列表(仅列表接口返回,不持久化。用于同人同地址合并时标识包含的订单) |
| 199 | 205 | /// </summary> |
| 200 | 206 | [SugarColumn(IsIgnore = true)] |
| ... | ... | @@ -229,5 +235,11 @@ public class Order |
| 229 | 235 | /// </summary> |
| 230 | 236 | [SugarColumn(IsIgnore = true)] |
| 231 | 237 | public bool IsPendingShipmentForm { get; set; } |
| 238 | + | |
| 239 | + /// <summary> | |
| 240 | + /// 是否可以「恢复合并」:NoMerge=true 且同买家+同地址下仍存在其他待发货订单时为 true(仅列表接口返回,不持久化)。 | |
| 241 | + /// </summary> | |
| 242 | + [SugarColumn(IsIgnore = true)] | |
| 243 | + public bool CanUnsplit { get; set; } | |
| 232 | 244 | } |
| 233 | 245 | ... | ... |
Antis.Erp.Plat/douyin/DouyinLogistics.API/Models/Waybill.cs
| ... | ... | @@ -151,5 +151,18 @@ public class Waybill |
| 151 | 151 | /// </summary> |
| 152 | 152 | [SugarColumn(Length = 100, IsNullable = true)] |
| 153 | 153 | public string? SalesOrderId { get; set; } |
| 154 | + | |
| 155 | + /// <summary> | |
| 156 | + /// 商家实际收入(元,可选)。用户可在编辑发货单时修改;空值表示按默认 = 用户支付金额。 | |
| 157 | + /// 提交 ERP 时会写入销售出库单的 skje(收款金额)。 | |
| 158 | + /// </summary> | |
| 159 | + [SugarColumn(ColumnDataType = "decimal(12,2)", IsNullable = true)] | |
| 160 | + public decimal? MerchantIncome { get; set; } | |
| 161 | + | |
| 162 | + /// <summary> | |
| 163 | + /// 发货人(ERP 用户 Id)。提交发货单时必填,会写入 ERP 销售出库单的 fhr 字段。 | |
| 164 | + /// </summary> | |
| 165 | + [SugarColumn(Length = 64, IsNullable = true)] | |
| 166 | + public string? Fhr { get; set; } | |
| 154 | 167 | } |
| 155 | 168 | ... | ... |
Antis.Erp.Plat/douyin/DouyinLogistics.API/Program.cs
| ... | ... | @@ -28,9 +28,13 @@ builder.Services.AddCors(options => |
| 28 | 28 | // 配置 HttpClient |
| 29 | 29 | builder.Services.AddHttpClient(); |
| 30 | 30 | |
| 31 | -// 配置抖音服务(支持多店铺:DouyinShops 数组) | |
| 32 | -var douyinShopConfigs = builder.Configuration.GetSection("DouyinShops").Get<List<DouyinConfig>>() ?? new List<DouyinConfig>(); | |
| 33 | -if (douyinShopConfigs.Count == 0) | |
| 31 | +// 内存缓存:用于订单列表分页的短期缓存,大幅降低翻页时的重复计算 | |
| 32 | +builder.Services.AddMemoryCache(); | |
| 33 | + | |
| 34 | +// 抖音店铺配置:首选来源 = ERP(WtDyDpsz/internal/all),兜底 = appsettings.json/DouyinShops | |
| 35 | +// 这样后续新开店铺时,只需要在 ERP 页面「抖音店铺设置」里维护,无需改配置文件和重启。 | |
| 36 | +var douyinShopFallback = builder.Configuration.GetSection("DouyinShops").Get<List<DouyinConfig>>() ?? new List<DouyinConfig>(); | |
| 37 | +if (douyinShopFallback.Count == 0) | |
| 34 | 38 | { |
| 35 | 39 | // 兼容旧版单店配置 |
| 36 | 40 | var douyinSection = builder.Configuration.GetSection("Douyin"); |
| ... | ... | @@ -38,7 +42,7 @@ if (douyinShopConfigs.Count == 0) |
| 38 | 42 | { |
| 39 | 43 | long envShopId = 0; |
| 40 | 44 | long.TryParse(Environment.GetEnvironmentVariable("DY_SHOP_ID"), out envShopId); |
| 41 | - douyinShopConfigs.Add(new DouyinConfig | |
| 45 | + douyinShopFallback.Add(new DouyinConfig | |
| 42 | 46 | { |
| 43 | 47 | SyncDays = douyinSection.GetValue<int?>("SyncDays") ?? 30, |
| 44 | 48 | AppKey = Environment.GetEnvironmentVariable("DY_APP_KEY") ?? douyinSection["AppKey"] ?? string.Empty, |
| ... | ... | @@ -57,12 +61,27 @@ if (douyinShopConfigs.Count == 0) |
| 57 | 61 | }); |
| 58 | 62 | } |
| 59 | 63 | } |
| 64 | + | |
| 65 | +// ShopConfigProvider:封装从 ERP 拉取店铺配置的逻辑(ERP 不可用时自动回退到 DouyinShops) | |
| 66 | +builder.Services.AddSingleton<ShopConfigProvider>(sp => | |
| 67 | +{ | |
| 68 | + var httpFactory = sp.GetRequiredService<IHttpClientFactory>(); | |
| 69 | + var loggerFactory = sp.GetRequiredService<ILoggerFactory>(); | |
| 70 | + var erpCfg = sp.GetRequiredService<ErpApiConfig>(); | |
| 71 | + return new ShopConfigProvider(httpFactory, loggerFactory.CreateLogger<ShopConfigProvider>(), erpCfg, douyinShopFallback); | |
| 72 | +}); | |
| 73 | + | |
| 60 | 74 | // 注册多店 Factory(Singleton,每个店一个 DouyinService 实例) |
| 75 | +// 构造函数里不立即加载,由启动钩子和 reload 接口驱动 | |
| 61 | 76 | builder.Services.AddSingleton<DouyinServiceFactory>(sp => |
| 62 | 77 | { |
| 63 | 78 | var httpFactory = sp.GetRequiredService<IHttpClientFactory>(); |
| 64 | 79 | var loggerFactory = sp.GetRequiredService<ILoggerFactory>(); |
| 65 | - return new DouyinServiceFactory(douyinShopConfigs, httpFactory, loggerFactory); | |
| 80 | + var provider = sp.GetRequiredService<ShopConfigProvider>(); | |
| 81 | + var factory = new DouyinServiceFactory(httpFactory, loggerFactory, provider); | |
| 82 | + // 先用兜底配置填充一份,避免极早请求到达时 factory 为空 | |
| 83 | + factory.ReplaceAll(douyinShopFallback); | |
| 84 | + return factory; | |
| 66 | 85 | }); |
| 67 | 86 | |
| 68 | 87 | // 配置顺丰服务 |
| ... | ... | @@ -185,7 +204,8 @@ _ = Task.Run(async () => |
| 185 | 204 | { "ProductItems", "text NULL COMMENT '商品明细(JSON格式,包含所有商品信息)'" }, |
| 186 | 205 | { "BuyerWords", "varchar(500) NULL COMMENT '买家留言'" }, |
| 187 | 206 | { "SellerWords", "varchar(500) NULL COMMENT '卖家备注'" }, |
| 188 | - { "LogisticsCode", "varchar(50) NULL COMMENT '物流公司代码(抖音company_code)'" } | |
| 207 | + { "LogisticsCode", "varchar(50) NULL COMMENT '物流公司代码(抖音company_code)'" }, | |
| 208 | + { "NoMerge", "tinyint NOT NULL DEFAULT 0 COMMENT '是否手动拆分出合并组:1=不参与自动合并'" } | |
| 189 | 209 | }; |
| 190 | 210 | |
| 191 | 211 | foreach (var field in fieldsToAdd) |
| ... | ... | @@ -228,7 +248,9 @@ _ = Task.Run(async () => |
| 228 | 248 | { |
| 229 | 249 | { "ShopId", "bigint NULL COMMENT '所属店铺ID(抖音ShopId)'" }, |
| 230 | 250 | { "Cjck", "varchar(100) NULL COMMENT '出库仓库(门店ID)'" }, |
| 231 | - { "SalesOrderId", "varchar(100) NULL COMMENT '销售出库单ID(ERP系统中的出库单ID)'" } | |
| 251 | + { "SalesOrderId", "varchar(100) NULL COMMENT '销售出库单ID(ERP系统中的出库单ID)'" }, | |
| 252 | + { "MerchantIncome", "decimal(12,2) NULL COMMENT '商家实际收入(元)。空值表示按用户支付金额默认'" }, | |
| 253 | + { "Fhr", "varchar(64) NULL COMMENT '发货人(ERP 用户 Id),提交销售出库单时写入 ERP fhr 字段'" } | |
| 232 | 254 | }; |
| 233 | 255 | |
| 234 | 256 | foreach (var field in fieldsToAdd) |
| ... | ... | @@ -300,9 +322,11 @@ CREATE TABLE IF NOT EXISTS `waybills` ( |
| 300 | 322 | } |
| 301 | 323 | |
| 302 | 324 | // 历史数据刷 ShopId:把 ShopId 为空的订单/运单归属到第一个配置的店铺 |
| 303 | - if (douyinShopConfigs.Count > 0) | |
| 325 | + var factoryForHistory = app.Services.GetRequiredService<DouyinServiceFactory>(); | |
| 326 | + var historyCfgs = factoryForHistory.GetAllConfigs(); | |
| 327 | + if (historyCfgs.Count > 0) | |
| 304 | 328 | { |
| 305 | - var defaultCfg = douyinShopConfigs[0]; | |
| 329 | + var defaultCfg = historyCfgs[0]; | |
| 306 | 330 | var updOrders = db.Ado.ExecuteCommand( |
| 307 | 331 | "UPDATE `orders` SET ShopId = @sid, ShopName = @sname WHERE ShopId IS NULL", |
| 308 | 332 | new { sid = defaultCfg.ShopId, sname = defaultCfg.ShopName }); |
| ... | ... | @@ -321,4 +345,20 @@ CREATE TABLE IF NOT EXISTS `waybills` ( |
| 321 | 345 | } |
| 322 | 346 | }); |
| 323 | 347 | |
| 348 | +// 启动时异步从 ERP 拉取抖音店铺配置并热替换 Factory(ERP 不可用时已经有 appsettings 兜底) | |
| 349 | +_ = Task.Run(async () => | |
| 350 | +{ | |
| 351 | + await Task.Delay(1500); | |
| 352 | + try | |
| 353 | + { | |
| 354 | + var factory = app.Services.GetRequiredService<DouyinServiceFactory>(); | |
| 355 | + var count = await factory.ReloadAsync(); | |
| 356 | + Console.WriteLine($"✅ 启动时已从 ERP 加载 {count} 个抖音店铺配置"); | |
| 357 | + } | |
| 358 | + catch (Exception ex) | |
| 359 | + { | |
| 360 | + Console.WriteLine($"⚠️ 启动时从 ERP 加载抖音店铺配置失败(已使用 appsettings 兜底): {ex.Message}"); | |
| 361 | + } | |
| 362 | +}); | |
| 363 | + | |
| 324 | 364 | app.Run(); | ... | ... |
Antis.Erp.Plat/douyin/DouyinLogistics.API/Services/DouyinService.cs
| ... | ... | @@ -24,6 +24,23 @@ public class DouyinService |
| 24 | 24 | /// <summary>接口未返回 expires_in 时采用的默认剩余秒数(约 2 小时)。</summary> |
| 25 | 25 | private const int DefaultExpiresInWhenUnknownSeconds = 7200; |
| 26 | 26 | |
| 27 | + /// <summary>北京时区(UTC+8)。用于把抖音返回的 Unix 时间戳转换为业务所需的北京时间。</summary> | |
| 28 | + private static readonly TimeZoneInfo BeijingTimeZone = ResolveBeijingTimeZone(); | |
| 29 | + | |
| 30 | + private static TimeZoneInfo ResolveBeijingTimeZone() | |
| 31 | + { | |
| 32 | + // .NET 6+ 在 Windows/Linux/macOS 均支持 "Asia/Shanghai";异常时兜底 Windows 标识 | |
| 33 | + try { return TimeZoneInfo.FindSystemTimeZoneById("Asia/Shanghai"); } | |
| 34 | + catch { return TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"); } | |
| 35 | + } | |
| 36 | + | |
| 37 | + /// <summary>把 Unix 秒时间戳转换为北京时间(Kind=Unspecified,便于直接写入 MySQL DATETIME)</summary> | |
| 38 | + private static DateTime UnixSecondsToBeijingTime(long seconds) | |
| 39 | + { | |
| 40 | + var utc = DateTimeOffset.FromUnixTimeSeconds(seconds).UtcDateTime; | |
| 41 | + return TimeZoneInfo.ConvertTimeFromUtc(utc, BeijingTimeZone); | |
| 42 | + } | |
| 43 | + | |
| 27 | 44 | public DouyinService(DouyinConfig config, IHttpClientFactory httpClientFactory, ILogger<DouyinService> logger) |
| 28 | 45 | { |
| 29 | 46 | _config = config; |
| ... | ... | @@ -483,22 +500,22 @@ public class DouyinService |
| 483 | 500 | } |
| 484 | 501 | } |
| 485 | 502 | |
| 486 | - // 抖音下单时间(create_time,秒或毫秒) | |
| 503 | + // 抖音下单时间(create_time,秒或毫秒)→ 转北京时间 | |
| 487 | 504 | var createTimeObj = orderObj["create_time"]; |
| 488 | 505 | if (createTimeObj != null && long.TryParse(createTimeObj.ToString(), out var createTimeRaw)) |
| 489 | 506 | { |
| 490 | 507 | var sec = createTimeRaw > 1_000_000_000_000L ? createTimeRaw / 1000 : createTimeRaw; |
| 491 | 508 | if (sec > 0) |
| 492 | - order.DouyinOrderTime = DateTimeOffset.FromUnixTimeSeconds(sec).DateTime; | |
| 509 | + order.DouyinOrderTime = UnixSecondsToBeijingTime(sec); | |
| 493 | 510 | } |
| 494 | 511 | |
| 495 | - // 提取付款时间(秒或毫秒) | |
| 512 | + // 提取付款时间(秒或毫秒)→ 转北京时间 | |
| 496 | 513 | var payTimeObj = orderObj["pay_time"]; |
| 497 | 514 | if (payTimeObj != null && long.TryParse(payTimeObj.ToString(), out var payTimeRaw)) |
| 498 | 515 | { |
| 499 | 516 | var paySec = payTimeRaw > 1_000_000_000_000L ? payTimeRaw / 1000 : payTimeRaw; |
| 500 | 517 | if (paySec > 0) |
| 501 | - order.PayTime = DateTimeOffset.FromUnixTimeSeconds(paySec).DateTime; | |
| 518 | + order.PayTime = UnixSecondsToBeijingTime(paySec); | |
| 502 | 519 | } |
| 503 | 520 | |
| 504 | 521 | // 提取订单金额和实付金额 | ... | ... |
Antis.Erp.Plat/douyin/DouyinLogistics.API/Services/DouyinServiceFactory.cs
| ... | ... | @@ -5,21 +5,75 @@ namespace DouyinLogistics.API.Services; |
| 5 | 5 | |
| 6 | 6 | /// <summary> |
| 7 | 7 | /// 管理多个抖店的 DouyinService 实例(每个 ShopId 一个),Singleton 注册。 |
| 8 | +/// 配置来源:启动时由 Program.cs 通过 ShopConfigProvider 从 ERP 拉取, | |
| 9 | +/// 运行时可以通过 ReloadAsync 重新拉取、热替换。 | |
| 8 | 10 | /// </summary> |
| 9 | 11 | public class DouyinServiceFactory |
| 10 | 12 | { |
| 11 | 13 | private readonly ConcurrentDictionary<long, DouyinService> _services = new(); |
| 12 | - private readonly List<DouyinConfig> _configs; | |
| 14 | + private List<DouyinConfig> _configs; | |
| 15 | + private readonly IHttpClientFactory _httpClientFactory; | |
| 16 | + private readonly ILoggerFactory _loggerFactory; | |
| 17 | + private readonly ShopConfigProvider _shopConfigProvider; | |
| 18 | + private readonly ILogger<DouyinServiceFactory> _logger; | |
| 19 | + private readonly SemaphoreSlim _reloadLock = new(1, 1); | |
| 13 | 20 | |
| 14 | - public DouyinServiceFactory(IEnumerable<DouyinConfig> configs, IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory) | |
| 21 | + public DouyinServiceFactory( | |
| 22 | + IHttpClientFactory httpClientFactory, | |
| 23 | + ILoggerFactory loggerFactory, | |
| 24 | + ShopConfigProvider shopConfigProvider) | |
| 15 | 25 | { |
| 16 | - _configs = configs.ToList(); | |
| 17 | - foreach (var cfg in _configs) | |
| 26 | + _httpClientFactory = httpClientFactory; | |
| 27 | + _loggerFactory = loggerFactory; | |
| 28 | + _shopConfigProvider = shopConfigProvider; | |
| 29 | + _logger = loggerFactory.CreateLogger<DouyinServiceFactory>(); | |
| 30 | + _configs = new List<DouyinConfig>(); | |
| 31 | + } | |
| 32 | + | |
| 33 | + /// <summary> | |
| 34 | + /// 使用指定配置列表初始化/重建所有 DouyinService 实例。 | |
| 35 | + /// 现有实例会被替换,实现热更新。 | |
| 36 | + /// </summary> | |
| 37 | + public void ReplaceAll(IEnumerable<DouyinConfig> configs) | |
| 38 | + { | |
| 39 | + var list = configs?.ToList() ?? new List<DouyinConfig>(); | |
| 40 | + _configs = list; | |
| 41 | + var newIds = new HashSet<long>(list.Select(c => c.ShopId)); | |
| 42 | + | |
| 43 | + foreach (var cfg in list) | |
| 18 | 44 | { |
| 19 | - var logger = loggerFactory.CreateLogger<DouyinService>(); | |
| 20 | - var svc = new DouyinService(cfg, httpClientFactory, logger); | |
| 45 | + if (cfg.ShopId <= 0) continue; | |
| 46 | + var svc = new DouyinService(cfg, _httpClientFactory, _loggerFactory.CreateLogger<DouyinService>()); | |
| 21 | 47 | _services[cfg.ShopId] = svc; |
| 22 | 48 | } |
| 49 | + | |
| 50 | + foreach (var key in _services.Keys.ToList()) | |
| 51 | + { | |
| 52 | + if (!newIds.Contains(key)) | |
| 53 | + { | |
| 54 | + _services.TryRemove(key, out _); | |
| 55 | + } | |
| 56 | + } | |
| 57 | + _logger.LogInformation("✅ DouyinServiceFactory 已加载 {Count} 个店铺:{Ids}", _services.Count, | |
| 58 | + string.Join(", ", _services.Keys)); | |
| 59 | + } | |
| 60 | + | |
| 61 | + /// <summary> | |
| 62 | + /// 从 ERP 刷新店铺配置并热替换服务实例。 | |
| 63 | + /// </summary> | |
| 64 | + public async Task<int> ReloadAsync(CancellationToken ct = default) | |
| 65 | + { | |
| 66 | + await _reloadLock.WaitAsync(ct); | |
| 67 | + try | |
| 68 | + { | |
| 69 | + var fresh = await _shopConfigProvider.FetchAllAsync(ct); | |
| 70 | + ReplaceAll(fresh); | |
| 71 | + return _services.Count; | |
| 72 | + } | |
| 73 | + finally | |
| 74 | + { | |
| 75 | + _reloadLock.Release(); | |
| 76 | + } | |
| 23 | 77 | } |
| 24 | 78 | |
| 25 | 79 | /// <summary>按 ShopId 获取对应的 DouyinService;找不到则返回 null。</summary> |
| ... | ... | @@ -32,6 +86,8 @@ public class DouyinServiceFactory |
| 32 | 86 | /// <summary>获取第一个(默认)DouyinService(兼容单店调用场景)。</summary> |
| 33 | 87 | public DouyinService GetDefaultService() |
| 34 | 88 | { |
| 89 | + if (_services.IsEmpty) | |
| 90 | + throw new InvalidOperationException("尚未加载任何抖音店铺配置,请到 ERP 抖音店铺设置中配置后,POST /api/shop-configs/reload 重新加载。"); | |
| 35 | 91 | return _services.Values.First(); |
| 36 | 92 | } |
| 37 | 93 | ... | ... |
Antis.Erp.Plat/douyin/DouyinLogistics.API/Services/OrderService.cs
| 1 | 1 | using DouyinLogistics.API.Models; |
| 2 | +using Microsoft.Extensions.Caching.Memory; | |
| 2 | 3 | using Microsoft.Extensions.DependencyInjection; |
| 3 | 4 | using SqlSugar; |
| 4 | 5 | using System.Text; |
| ... | ... | @@ -33,10 +34,17 @@ public class OrderService |
| 33 | 34 | private readonly HttpClient _httpClient; |
| 34 | 35 | private readonly ErpApiConfig _erpApiConfig; |
| 35 | 36 | private readonly IServiceScopeFactory _scopeFactory; |
| 37 | + private readonly IMemoryCache _cache; | |
| 36 | 38 | private static string? _erpToken = null; |
| 37 | 39 | private static DateTime _tokenExpireTime = DateTime.MinValue; |
| 38 | 40 | private static readonly object _tokenLock = new object(); |
| 39 | 41 | |
| 42 | + // 订单列表缓存 TTL:短 TTL 让翻页秒开,又保证订单同步/状态变更后很快可见 | |
| 43 | + private static readonly TimeSpan OrderListCacheTtl = TimeSpan.FromSeconds(15); | |
| 44 | + // 订单列表缓存版本,用于在订单/发货单写入后强制失效(修改时 Interlocked.Increment) | |
| 45 | + private static long _orderListCacheVersion = 0; | |
| 46 | + private const string OrderListCachePrefix = "orders:list:"; | |
| 47 | + | |
| 40 | 48 | public OrderService( |
| 41 | 49 | ISqlSugarClient db, |
| 42 | 50 | DouyinServiceFactory douyinFactory, |
| ... | ... | @@ -45,7 +53,8 @@ public class OrderService |
| 45 | 53 | ILogger<OrderService> logger, |
| 46 | 54 | IHttpClientFactory httpClientFactory, |
| 47 | 55 | ErpApiConfig erpApiConfig, |
| 48 | - IServiceScopeFactory scopeFactory) | |
| 56 | + IServiceScopeFactory scopeFactory, | |
| 57 | + IMemoryCache cache) | |
| 49 | 58 | { |
| 50 | 59 | _db = db; |
| 51 | 60 | _douyinFactory = douyinFactory; |
| ... | ... | @@ -55,6 +64,15 @@ public class OrderService |
| 55 | 64 | _httpClient = httpClientFactory.CreateClient(); |
| 56 | 65 | _erpApiConfig = erpApiConfig; |
| 57 | 66 | _scopeFactory = scopeFactory; |
| 67 | + _cache = cache; | |
| 68 | + } | |
| 69 | + | |
| 70 | + /// <summary> | |
| 71 | + /// 失效订单列表缓存;订单同步、状态变更、发货单创建/更新后调用,避免用户看到过期数据 | |
| 72 | + /// </summary> | |
| 73 | + public static void InvalidateOrderListCache() | |
| 74 | + { | |
| 75 | + System.Threading.Interlocked.Increment(ref _orderListCacheVersion); | |
| 58 | 76 | } |
| 59 | 77 | |
| 60 | 78 | /// <summary> |
| ... | ... | @@ -465,6 +483,8 @@ public class OrderService |
| 465 | 483 | { |
| 466 | 484 | _logger.LogInformation("订单同步完成 - 新增: {NewCount}, 更新: {UpdateCount}, 总计: {TotalCount}, 总耗时: {TotalElapsed} 秒", |
| 467 | 485 | newOrderCount, updateOrderCount, douyinOrders.Count, totalElapsed.ToString("F2")); |
| 486 | + // 有新增/更新:让下一次列表请求立刻感知变化 | |
| 487 | + InvalidateOrderListCache(); | |
| 468 | 488 | } |
| 469 | 489 | else |
| 470 | 490 | { |
| ... | ... | @@ -660,23 +680,47 @@ public class OrderService |
| 660 | 680 | query = query.Where(o => o.DouyinOrderTime != null && o.DouyinOrderTime <= douyinOrderTimeEnd.Value.AddDays(1).AddSeconds(-1)); |
| 661 | 681 | } |
| 662 | 682 | |
| 663 | - // 所有状态都参与合并(按买家+地址分组),已退款/退款中/已取消从合并组中剔除 | |
| 664 | - var allOrders = await query | |
| 665 | - .OrderBy("COALESCE(PayTime, DouyinOrderTime, CreateTime) ASC") | |
| 666 | - .ToListAsync(); | |
| 667 | - var merged = MergeOrdersByBuyerAndAddress(allOrders); | |
| 668 | - if (pendingShipmentForm || hasWaybill.HasValue) | |
| 669 | - merged = await FilterMergedOrdersPostProcessAsync(merged, hasWaybill, pendingShipmentForm); | |
| 670 | - merged = await EnrichMergedShipmentFormStateAsync(merged); | |
| 671 | - if (shipmentFormSubmitted == true) | |
| 672 | - { | |
| 673 | - merged = merged | |
| 674 | - .Where(m => m.Status == 1 && m.HasSubmittedShipmentForm) | |
| 675 | - .ToList(); | |
| 683 | + // 过滤条件完全一致时,10~15 秒内的重复翻页请求命中缓存,避免重复拉全表 + 重复合并 | |
| 684 | + var cacheKey = BuildOrderListCacheKey( | |
| 685 | + shopId, status, orderId, receiverName, receiverPhone, trackingNumber, productName, | |
| 686 | + hasWaybill, pendingShipmentForm, createTimeStart, createTimeEnd, | |
| 687 | + payTimeStart, payTimeEnd, douyinOrderTimeStart, douyinOrderTimeEnd, | |
| 688 | + shipmentFormSubmitted); | |
| 689 | + | |
| 690 | + List<Order> merged; | |
| 691 | + if (_cache.TryGetValue<List<Order>>(cacheKey, out var cached) && cached != null) | |
| 692 | + { | |
| 693 | + merged = cached; | |
| 676 | 694 | } |
| 677 | - merged = ApplyMergedOrderSort(merged, sortBy, sortOrder); | |
| 678 | - var total = merged.Count; | |
| 679 | - var paged = merged | |
| 695 | + else | |
| 696 | + { | |
| 697 | + // 所有状态都参与合并(按买家+地址分组),已退款/退款中/已取消从合并组中剔除 | |
| 698 | + var allOrders = await query | |
| 699 | + .OrderBy("COALESCE(PayTime, DouyinOrderTime, CreateTime) ASC") | |
| 700 | + .ToListAsync(); | |
| 701 | + merged = MergeOrdersByBuyerAndAddress(allOrders); | |
| 702 | + | |
| 703 | + // 合并 Waybill 查询:以前会分别在 FilterMergedOrdersPostProcessAsync、EnrichMergedShipmentFormStateAsync 里各查一次 | |
| 704 | + // 这里一次性查好,两个逻辑共用,减少一次 WHERE IN (几百~几千 ID) 的重 SQL | |
| 705 | + merged = await EnrichAndFilterMergedAsync(merged, hasWaybill, pendingShipmentForm); | |
| 706 | + if (shipmentFormSubmitted == true) | |
| 707 | + { | |
| 708 | + merged = merged | |
| 709 | + .Where(m => m.Status == 1 && m.HasSubmittedShipmentForm) | |
| 710 | + .ToList(); | |
| 711 | + } | |
| 712 | + | |
| 713 | + _cache.Set(cacheKey, merged, new MemoryCacheEntryOptions | |
| 714 | + { | |
| 715 | + AbsoluteExpirationRelativeToNow = OrderListCacheTtl, | |
| 716 | + Size = merged.Count | |
| 717 | + }); | |
| 718 | + } | |
| 719 | + | |
| 720 | + // 排序和分页保持在内存里做,命中缓存时也能按不同的 sortBy/pageIndex 快速响应 | |
| 721 | + var sorted = ApplyMergedOrderSort(merged, sortBy, sortOrder); | |
| 722 | + var total = sorted.Count; | |
| 723 | + var paged = sorted | |
| 680 | 724 | .Skip((pageIndex - 1) * pageSize) |
| 681 | 725 | .Take(pageSize) |
| 682 | 726 | .ToList(); |
| ... | ... | @@ -684,6 +728,93 @@ public class OrderService |
| 684 | 728 | } |
| 685 | 729 | |
| 686 | 730 | /// <summary> |
| 731 | + /// 构造订单列表缓存 Key:包含所有影响"候选结果集"的过滤条件和版本号。 | |
| 732 | + /// 不包含 sortBy/sortOrder/pageIndex/pageSize,这些在内存里处理。 | |
| 733 | + /// </summary> | |
| 734 | + private static string BuildOrderListCacheKey( | |
| 735 | + long? shopId, int? status, string? orderId, string? receiverName, string? receiverPhone, | |
| 736 | + string? trackingNumber, string? productName, bool? hasWaybill, bool pendingShipmentForm, | |
| 737 | + DateTime? createTimeStart, DateTime? createTimeEnd, | |
| 738 | + DateTime? payTimeStart, DateTime? payTimeEnd, | |
| 739 | + DateTime? douyinOrderTimeStart, DateTime? douyinOrderTimeEnd, | |
| 740 | + bool? shipmentFormSubmitted) | |
| 741 | + { | |
| 742 | + string F(DateTime? d) => d?.ToString("yyyyMMddHHmmss") ?? ""; | |
| 743 | + return string.Concat( | |
| 744 | + OrderListCachePrefix, | |
| 745 | + System.Threading.Interlocked.Read(ref _orderListCacheVersion), "|", | |
| 746 | + shopId?.ToString() ?? "", "|", | |
| 747 | + status?.ToString() ?? "", "|", | |
| 748 | + orderId?.Trim() ?? "", "|", | |
| 749 | + receiverName?.Trim() ?? "", "|", | |
| 750 | + receiverPhone?.Trim() ?? "", "|", | |
| 751 | + trackingNumber?.Trim() ?? "", "|", | |
| 752 | + productName?.Trim() ?? "", "|", | |
| 753 | + hasWaybill?.ToString() ?? "", "|", | |
| 754 | + pendingShipmentForm, "|", | |
| 755 | + F(createTimeStart), "|", F(createTimeEnd), "|", | |
| 756 | + F(payTimeStart), "|", F(payTimeEnd), "|", | |
| 757 | + F(douyinOrderTimeStart), "|", F(douyinOrderTimeEnd), "|", | |
| 758 | + shipmentFormSubmitted?.ToString() ?? ""); | |
| 759 | + } | |
| 760 | + | |
| 761 | + /// <summary> | |
| 762 | + /// 合并 FilterMergedOrdersPostProcessAsync + EnrichMergedShipmentFormStateAsync 的逻辑: | |
| 763 | + /// 只查一次 Waybill 表,同时完成过滤(hasWaybill / pendingShipmentForm)和状态补充(HasSubmittedShipmentForm / IsPendingShipmentForm)。 | |
| 764 | + /// </summary> | |
| 765 | + private async Task<List<Order>> EnrichAndFilterMergedAsync( | |
| 766 | + List<Order> merged, bool? hasWaybill, bool pendingShipmentForm) | |
| 767 | + { | |
| 768 | + if (merged == null || merged.Count == 0) | |
| 769 | + return merged ?? new List<Order>(); | |
| 770 | + | |
| 771 | + var allIds = merged | |
| 772 | + .SelectMany(m => m.MergedOrderIds != null && m.MergedOrderIds.Count > 0 | |
| 773 | + ? m.MergedOrderIds | |
| 774 | + : new List<int> { m.Id }) | |
| 775 | + .Distinct() | |
| 776 | + .ToList(); | |
| 777 | + | |
| 778 | + var wbRows = await _db.Queryable<Waybill>() | |
| 779 | + .Where(w => allIds.Contains(w.OrderId)) | |
| 780 | + .Select(w => new { w.OrderId, w.SalesOrderId }) | |
| 781 | + .ToListAsync(); | |
| 782 | + | |
| 783 | + var hasWaybillSet = new HashSet<int>(wbRows.Select(w => w.OrderId)); | |
| 784 | + var submittedSet = new HashSet<int>(wbRows | |
| 785 | + .Where(w => !string.IsNullOrWhiteSpace(w.SalesOrderId)) | |
| 786 | + .Select(w => w.OrderId)); | |
| 787 | + | |
| 788 | + var result = new List<Order>(merged.Count); | |
| 789 | + foreach (var m in merged) | |
| 790 | + { | |
| 791 | + var ids = m.MergedOrderIds != null && m.MergedOrderIds.Count > 0 | |
| 792 | + ? m.MergedOrderIds | |
| 793 | + : new List<int> { m.Id }; | |
| 794 | + | |
| 795 | + var hasWb = ids.Any(id => hasWaybillSet.Contains(id)) || !string.IsNullOrWhiteSpace(m.TrackingNumber); | |
| 796 | + var hasSubmitted = ids.Any(id => submittedSet.Contains(id)); | |
| 797 | + | |
| 798 | + // 过滤 | |
| 799 | + if (pendingShipmentForm) | |
| 800 | + { | |
| 801 | + if (!hasWb || hasSubmitted) continue; | |
| 802 | + } | |
| 803 | + else if (hasWaybill.HasValue) | |
| 804 | + { | |
| 805 | + if (hasWaybill.Value != hasWb) continue; | |
| 806 | + } | |
| 807 | + | |
| 808 | + // 补充状态字段 | |
| 809 | + m.HasSubmittedShipmentForm = hasSubmitted; | |
| 810 | + m.IsPendingShipmentForm = m.Status == 1 && hasWb && !hasSubmitted; | |
| 811 | + result.Add(m); | |
| 812 | + } | |
| 813 | + | |
| 814 | + return result; | |
| 815 | + } | |
| 816 | + | |
| 817 | + /// <summary> | |
| 687 | 818 | /// 合并订单规则: |
| 688 | 819 | /// 1. 待发货(0):同买家同地址 → 合并(用于一起发货) |
| 689 | 820 | /// 2. 已发货(1):同运单号 → 合并(之前合并发货的继续合并展示,分开发货的分开展示) |
| ... | ... | @@ -711,7 +842,25 @@ public class OrderService |
| 711 | 842 | result.AddRange(refundingOrders); |
| 712 | 843 | |
| 713 | 844 | // 规则1:待发货按同买家同地址合并 |
| 714 | - var pendingGroups = pendingOrders | |
| 845 | + // 用户手动标记 NoMerge=true 的订单不参与合并,单独显示(走 "拆分" 按钮把订单从合并组里拆出来) | |
| 846 | + var pendingNoMerge = pendingOrders.Where(o => o.NoMerge).ToList(); | |
| 847 | + var pendingMergeable = pendingOrders.Where(o => !o.NoMerge).ToList(); | |
| 848 | + | |
| 849 | + // 对每个 NoMerge 订单判断是否还"能恢复合并": | |
| 850 | + // - 同买家+同地址下,待发货订单总数(含自己、含其他 NoMerge)>=2 才有合并价值 | |
| 851 | + // - 只剩它一个待发货的(伙伴都发货/取消了)就不显示"恢复合并"按钮 | |
| 852 | + var pendingKeyCount = pendingOrders | |
| 853 | + .GroupBy(o => GetBuyerAddressKey(o)) | |
| 854 | + .ToDictionary(g => g.Key, g => g.Count()); | |
| 855 | + foreach (var o in pendingNoMerge) | |
| 856 | + { | |
| 857 | + var cnt = pendingKeyCount.TryGetValue(GetBuyerAddressKey(o), out var n) ? n : 1; | |
| 858 | + o.CanUnsplit = cnt >= 2; | |
| 859 | + } | |
| 860 | + | |
| 861 | + result.AddRange(pendingNoMerge); | |
| 862 | + | |
| 863 | + var pendingGroups = pendingMergeable | |
| 715 | 864 | .GroupBy(o => GetBuyerAddressKey(o)) |
| 716 | 865 | .ToList(); |
| 717 | 866 | foreach (var g in pendingGroups) |
| ... | ... | @@ -726,10 +875,11 @@ public class OrderService |
| 726 | 875 | } |
| 727 | 876 | |
| 728 | 877 | // 规则2:已发货按同运单号合并(只有共享运单号的才是一起发货的) |
| 729 | - var shippedWithTracking = shippedOrders.Where(o => !string.IsNullOrWhiteSpace(o.TrackingNumber)).ToList(); | |
| 730 | - var shippedNoTracking = shippedOrders.Where(o => string.IsNullOrWhiteSpace(o.TrackingNumber)).ToList(); | |
| 878 | + // 已拆分的订单(NoMerge=true)也走单独显示,避免历史标记影响已发货展示 | |
| 879 | + var shippedWithTracking = shippedOrders.Where(o => !string.IsNullOrWhiteSpace(o.TrackingNumber) && !o.NoMerge).ToList(); | |
| 880 | + var shippedNoTracking = shippedOrders.Where(o => string.IsNullOrWhiteSpace(o.TrackingNumber) || o.NoMerge).ToList(); | |
| 731 | 881 | |
| 732 | - // 无运单号的已发货订单单独显示 | |
| 882 | + // 无运单号 / 已被用户拆分的已发货订单单独显示 | |
| 733 | 883 | result.AddRange(shippedNoTracking); |
| 734 | 884 | |
| 735 | 885 | // 有运单号的按运单号分组 |
| ... | ... | @@ -753,6 +903,67 @@ public class OrderService |
| 753 | 903 | .ToList(); |
| 754 | 904 | } |
| 755 | 905 | |
| 906 | + /// <summary> | |
| 907 | + /// 拆分合并单:把指定订单 id 集合上的 NoMerge 置 true,后续列表不再把它们合并到同买家+同地址的组里。 | |
| 908 | + /// 已发货订单(status=1)不应拆分:合并是按运单号识别,拆了也没实际意义,这里直接忽略。 | |
| 909 | + /// </summary> | |
| 910 | + /// <returns>实际被更新的订单数量</returns> | |
| 911 | + public async Task<int> SplitMergedOrdersAsync(IEnumerable<int> orderIds) | |
| 912 | + { | |
| 913 | + var ids = orderIds?.Distinct().ToList() ?? new List<int>(); | |
| 914 | + if (ids.Count == 0) return 0; | |
| 915 | + | |
| 916 | + var rows = await _db.Queryable<Order>() | |
| 917 | + .Where(o => ids.Contains(o.Id)) | |
| 918 | + .ToListAsync(); | |
| 919 | + | |
| 920 | + // 只对「待发货/未拆分」订单做拆分;已拆分/已发货/已取消/已退款一律不动 | |
| 921 | + var toUpdate = rows | |
| 922 | + .Where(o => !o.NoMerge && o.Status == 0) | |
| 923 | + .ToList(); | |
| 924 | + if (toUpdate.Count == 0) return 0; | |
| 925 | + | |
| 926 | + foreach (var o in toUpdate) | |
| 927 | + { | |
| 928 | + o.NoMerge = true; | |
| 929 | + o.UpdateTime = DateTime.Now; | |
| 930 | + } | |
| 931 | + | |
| 932 | + await _db.Updateable(toUpdate) | |
| 933 | + .UpdateColumns(o => new { o.NoMerge, o.UpdateTime }) | |
| 934 | + .ExecuteCommandAsync(); | |
| 935 | + | |
| 936 | + InvalidateOrderListCache(); | |
| 937 | + return toUpdate.Count; | |
| 938 | + } | |
| 939 | + | |
| 940 | + /// <summary> | |
| 941 | + /// 恢复合并:把指定订单的 NoMerge 置 false,下次列表按同买家+同地址重新合并。 | |
| 942 | + /// </summary> | |
| 943 | + public async Task<int> UnsplitOrdersAsync(IEnumerable<int> orderIds) | |
| 944 | + { | |
| 945 | + var ids = orderIds?.Distinct().ToList() ?? new List<int>(); | |
| 946 | + if (ids.Count == 0) return 0; | |
| 947 | + | |
| 948 | + var rows = await _db.Queryable<Order>() | |
| 949 | + .Where(o => ids.Contains(o.Id) && o.NoMerge) | |
| 950 | + .ToListAsync(); | |
| 951 | + if (rows.Count == 0) return 0; | |
| 952 | + | |
| 953 | + foreach (var o in rows) | |
| 954 | + { | |
| 955 | + o.NoMerge = false; | |
| 956 | + o.UpdateTime = DateTime.Now; | |
| 957 | + } | |
| 958 | + | |
| 959 | + await _db.Updateable(rows) | |
| 960 | + .UpdateColumns(o => new { o.NoMerge, o.UpdateTime }) | |
| 961 | + .ExecuteCommandAsync(); | |
| 962 | + | |
| 963 | + InvalidateOrderListCache(); | |
| 964 | + return rows.Count; | |
| 965 | + } | |
| 966 | + | |
| 756 | 967 | private string GetBuyerAddressKey(Order o) |
| 757 | 968 | { |
| 758 | 969 | var shopPart = (o.ShopId ?? 0L).ToString(); |
| ... | ... | @@ -792,6 +1003,34 @@ public class OrderService |
| 792 | 1003 | first.MergedOrderStatuses = list.Select(o => o.Status).ToList(); |
| 793 | 1004 | first.PayAmount = list.Sum(o => o.PayAmount ?? 0); |
| 794 | 1005 | first.OrderAmount = list.Sum(o => o.OrderAmount ?? 0); |
| 1006 | + | |
| 1007 | + // 合并所有订单的买家留言和卖家备注(每个订单一行,带订单号前缀) | |
| 1008 | + if (list.Count > 1) | |
| 1009 | + { | |
| 1010 | + var buyerLines = list | |
| 1011 | + .Where(o => !string.IsNullOrWhiteSpace(o.BuyerWords)) | |
| 1012 | + .Select(o => | |
| 1013 | + { | |
| 1014 | + var tail = !string.IsNullOrEmpty(o.OrderId) && o.OrderId.Length >= 6 | |
| 1015 | + ? o.OrderId.Substring(o.OrderId.Length - 6) | |
| 1016 | + : (o.OrderId ?? ""); | |
| 1017 | + return $"[{tail}] {o.BuyerWords}"; | |
| 1018 | + }) | |
| 1019 | + .ToList(); | |
| 1020 | + first.BuyerWords = buyerLines.Count > 0 ? string.Join("\n", buyerLines) : ""; | |
| 1021 | + | |
| 1022 | + var sellerLines = list | |
| 1023 | + .Where(o => !string.IsNullOrWhiteSpace(o.SellerWords)) | |
| 1024 | + .Select(o => | |
| 1025 | + { | |
| 1026 | + var tail = !string.IsNullOrEmpty(o.OrderId) && o.OrderId.Length >= 6 | |
| 1027 | + ? o.OrderId.Substring(o.OrderId.Length - 6) | |
| 1028 | + : (o.OrderId ?? ""); | |
| 1029 | + return $"[{tail}] {o.SellerWords}"; | |
| 1030 | + }) | |
| 1031 | + .ToList(); | |
| 1032 | + first.SellerWords = sellerLines.Count > 0 ? string.Join("\n", sellerLines) : ""; | |
| 1033 | + } | |
| 795 | 1034 | var douyinTimes = list.Where(x => x.DouyinOrderTime.HasValue).Select(x => x.DouyinOrderTime!.Value).ToList(); |
| 796 | 1035 | if (douyinTimes.Count > 0) |
| 797 | 1036 | first.DouyinOrderTime = douyinTimes.Min(); |
| ... | ... | @@ -1408,7 +1647,9 @@ public class OrderService |
| 1408 | 1647 | string? overrideSkzh = null, |
| 1409 | 1648 | string? djlx = null, |
| 1410 | 1649 | string? douyinOrderIds = null, |
| 1411 | - List<int>? mergedOrderInternalIds = null) | |
| 1650 | + List<int>? mergedOrderInternalIds = null, | |
| 1651 | + decimal? merchantIncome = null, | |
| 1652 | + string? fhr = null) | |
| 1412 | 1653 | { |
| 1413 | 1654 | var result = new CreateSalesOrderResult(); |
| 1414 | 1655 | var finalDjlx = string.IsNullOrEmpty(djlx) ? "销售出库单" : djlx; |
| ... | ... | @@ -1488,30 +1729,30 @@ public class OrderService |
| 1488 | 1729 | return result; |
| 1489 | 1730 | } |
| 1490 | 1731 | |
| 1491 | - // 4. 获取出库仓库 | |
| 1732 | + // 4. 获取出库仓库(优先级:发货单 cjck > 抖音店铺设置 ck;不再兜底全局默认仓库,避免误发) | |
| 1492 | 1733 | if (waybill != null && !string.IsNullOrEmpty(waybill.Cjck)) |
| 1493 | 1734 | { |
| 1494 | 1735 | warehouseId = waybill.Cjck; |
| 1495 | 1736 | _logger.LogInformation("从发货单获取出库仓库: OrderId={OrderId}, WarehouseId={WarehouseId}", orderId, warehouseId); |
| 1496 | 1737 | } |
| 1497 | - else | |
| 1738 | + else if (order.ShopId.HasValue) | |
| 1498 | 1739 | { |
| 1499 | 1740 | try |
| 1500 | 1741 | { |
| 1501 | - var warehouseApiUrl = $"{_erpApiConfig.BaseUrl}/api/Extend/WtMd?currentPage=1&pageSize=1"; | |
| 1502 | - var warehouseRequest = await CreateAuthenticatedRequestAsync(HttpMethod.Get, warehouseApiUrl); | |
| 1503 | - var warehouseResponse = await _httpClient.SendAsync(warehouseRequest); | |
| 1504 | - if (warehouseResponse.IsSuccessStatusCode) | |
| 1742 | + var shopCkUrl = $"{_erpApiConfig.BaseUrl}/api/Extend/WtDyDpsz/by-shop/{order.ShopId.Value}"; | |
| 1743 | + var shopCkReq = await CreateAuthenticatedRequestAsync(HttpMethod.Get, shopCkUrl); | |
| 1744 | + var shopCkRes = await _httpClient.SendAsync(shopCkReq); | |
| 1745 | + if (shopCkRes.IsSuccessStatusCode) | |
| 1505 | 1746 | { |
| 1506 | - var warehouseContent = await warehouseResponse.Content.ReadAsStringAsync(); | |
| 1507 | - var warehouseResult = JsonConvert.DeserializeObject<JObject>(warehouseContent); | |
| 1508 | - if (warehouseResult != null && warehouseResult["code"]?.ToString() == "200") | |
| 1747 | + var shopCkContent = await shopCkRes.Content.ReadAsStringAsync(); | |
| 1748 | + var shopCkRoot = JsonConvert.DeserializeObject<JObject>(shopCkContent); | |
| 1749 | + if (shopCkRoot != null && shopCkRoot["code"]?.ToString() == "200") | |
| 1509 | 1750 | { |
| 1510 | - var warehouseList = warehouseResult["data"]?["list"] as JArray; | |
| 1511 | - if (warehouseList != null && warehouseList.Count > 0) | |
| 1751 | + var shopCk = shopCkRoot["data"]?["ck"]?.ToString(); | |
| 1752 | + if (!string.IsNullOrEmpty(shopCk)) | |
| 1512 | 1753 | { |
| 1513 | - warehouseId = warehouseList[0]?["id"]?.ToString() ?? ""; | |
| 1514 | - _logger.LogInformation("获取默认门店ID: OrderId={OrderId}, WarehouseId={WarehouseId}", orderId, warehouseId); | |
| 1754 | + warehouseId = shopCk; | |
| 1755 | + _logger.LogInformation("从抖音店铺设置获取出库仓库: ShopId={ShopId}, WarehouseId={WarehouseId}", order.ShopId.Value, warehouseId); | |
| 1515 | 1756 | } |
| 1516 | 1757 | } |
| 1517 | 1758 | } |
| ... | ... | @@ -1603,68 +1844,113 @@ public class OrderService |
| 1603 | 1844 | return result; |
| 1604 | 1845 | } |
| 1605 | 1846 | |
| 1606 | - // 5.5 自动库存校验:解析完商品后调用 BatchCheckStock,库存不足自动切换为预售出库单 | |
| 1847 | + // 5.5 库存校验:抖音发货只支持销售出库单(不再自动转预售) | |
| 1848 | + // 只要有商品库存不足或缺少商品编码,直接报错,让用户去 ERP 补货或在抖音侧取消发货 | |
| 1607 | 1849 | if (finalDjlx == "销售出库单" && !string.IsNullOrEmpty(warehouseId)) |
| 1608 | 1850 | { |
| 1609 | - // 没有 spbm(商品编码)的商品无法通过 API 校验库存,直接视为库存不可用 | |
| 1610 | 1851 | var itemsWithoutSpbm = resolvedItems.Where(r => string.IsNullOrEmpty(r.sptm)).ToList(); |
| 1611 | 1852 | if (itemsWithoutSpbm.Count > 0) |
| 1612 | 1853 | { |
| 1613 | - finalDjlx = "预售出库单"; | |
| 1614 | 1854 | var names = string.Join("、", itemsWithoutSpbm.Select(r => r.spmc)); |
| 1615 | - _logger.LogInformation("商品无编码(spbm)无法校验库存,自动切换为预售出库单: OrderId={OrderId}, 商品={Names}", orderId, names); | |
| 1855 | + _logger.LogWarning("商品无编码(spbm)无法校验库存,拒绝发货: OrderId={OrderId}, 商品={Names}", orderId, names); | |
| 1856 | + result.ErrorMessage = $"以下商品未关联 ERP 商品编码,无法校验库存,请先在 ERP 商品档案补齐:{names}"; | |
| 1857 | + return result; | |
| 1616 | 1858 | } |
| 1617 | - else | |
| 1859 | + | |
| 1860 | + try | |
| 1618 | 1861 | { |
| 1619 | - try | |
| 1862 | + var stockItems = resolvedItems | |
| 1863 | + .Select(r => new { spbm = r.sptm, quantity = r.sl }) | |
| 1864 | + .ToList(); | |
| 1865 | + | |
| 1866 | + var stockPayload = new { storeId = warehouseId, items = stockItems }; | |
| 1867 | + var stockJson = JsonConvert.SerializeObject(stockPayload); | |
| 1868 | + var stockContent = new StringContent(stockJson, Encoding.UTF8, "application/json"); | |
| 1869 | + var stockUrl = $"{_erpApiConfig.BaseUrl}/api/Extend/wtsp/BatchCheckStock"; | |
| 1870 | + var stockRequest = await CreateAuthenticatedRequestAsync(HttpMethod.Post, stockUrl, stockContent); | |
| 1871 | + var stockResponse = await _httpClient.SendAsync(stockRequest); | |
| 1872 | + | |
| 1873 | + if (!stockResponse.IsSuccessStatusCode) | |
| 1620 | 1874 | { |
| 1621 | - var stockItems = resolvedItems | |
| 1622 | - .Select(r => new { spbm = r.sptm, quantity = r.sl }) | |
| 1623 | - .ToList(); | |
| 1624 | - | |
| 1625 | - var stockPayload = new { storeId = warehouseId, items = stockItems }; | |
| 1626 | - var stockJson = JsonConvert.SerializeObject(stockPayload); | |
| 1627 | - var stockContent = new StringContent(stockJson, Encoding.UTF8, "application/json"); | |
| 1628 | - var stockUrl = $"{_erpApiConfig.BaseUrl}/api/Extend/wtsp/BatchCheckStock"; | |
| 1629 | - var stockRequest = await CreateAuthenticatedRequestAsync(HttpMethod.Post, stockUrl, stockContent); | |
| 1630 | - var stockResponse = await _httpClient.SendAsync(stockRequest); | |
| 1631 | - | |
| 1632 | - if (stockResponse.IsSuccessStatusCode) | |
| 1633 | - { | |
| 1634 | - var stockResponseContent = await stockResponse.Content.ReadAsStringAsync(); | |
| 1635 | - var stockResult = JsonConvert.DeserializeObject<JObject>(stockResponseContent); | |
| 1636 | - var stockApiOk = stockResult?["success"]?.ToObject<bool>() ?? false; | |
| 1637 | - if (!stockApiOk) | |
| 1638 | - { | |
| 1639 | - var errMsg = stockResult?["msg"]?.ToString() ?? ""; | |
| 1640 | - _logger.LogWarning("库存校验返回失败: OrderId={OrderId}, Msg={Msg}", orderId, errMsg); | |
| 1641 | - finalDjlx = "预售出库单"; | |
| 1642 | - } | |
| 1643 | - else | |
| 1644 | - { | |
| 1645 | - // 缺省按「不充足」处理,避免缺字段时误判为销售出库单 | |
| 1646 | - var allSufficient = stockResult?["allSufficient"]?.ToObject<bool>() ?? false; | |
| 1647 | - if (!allSufficient) | |
| 1648 | - { | |
| 1649 | - finalDjlx = "预售出库单"; | |
| 1650 | - _logger.LogInformation("库存不足,自动切换为预售出库单: OrderId={OrderId}", orderId); | |
| 1651 | - } | |
| 1652 | - else | |
| 1653 | - { | |
| 1654 | - _logger.LogInformation("库存充足,使用销售出库单: OrderId={OrderId}", orderId); | |
| 1655 | - } | |
| 1656 | - } | |
| 1657 | - } | |
| 1658 | - else | |
| 1875 | + _logger.LogWarning("库存校验API返回非成功状态: StatusCode={StatusCode}, OrderId={OrderId}", | |
| 1876 | + stockResponse.StatusCode, orderId); | |
| 1877 | + result.ErrorMessage = $"调用 ERP 库存校验接口失败(HTTP {(int)stockResponse.StatusCode}),请稍后重试"; | |
| 1878 | + return result; | |
| 1879 | + } | |
| 1880 | + | |
| 1881 | + var stockResponseContent = await stockResponse.Content.ReadAsStringAsync(); | |
| 1882 | + _logger.LogDebug("BatchCheckStock 原始返回: OrderId={OrderId}, Body={Body}", orderId, stockResponseContent); | |
| 1883 | + var stockResult = JsonConvert.DeserializeObject<JObject>(stockResponseContent); | |
| 1884 | + | |
| 1885 | + // NCC 动态 API 会把 Controller 返回值再包一层 { code, msg, data }: | |
| 1886 | + // - 外层:{ code: 200, msg: "操作成功", data: { success: true, data: [...], allSufficient: true } } | |
| 1887 | + // - 这里只关心真正 BatchCheckStock 返回的那层(payload),并同时兼容没有包装时的裸返回 | |
| 1888 | + JObject? payload = stockResult; | |
| 1889 | + var ncCode = stockResult?["code"]?.ToObject<int?>(); | |
| 1890 | + if (ncCode.HasValue) | |
| 1891 | + { | |
| 1892 | + if (ncCode.Value != 200) | |
| 1659 | 1893 | { |
| 1660 | - _logger.LogWarning("库存校验API返回非成功状态: StatusCode={StatusCode}, OrderId={OrderId}", | |
| 1661 | - stockResponse.StatusCode, orderId); | |
| 1894 | + var ncMsg = stockResult?["msg"]?.ToString() ?? "调用 ERP 库存校验接口失败"; | |
| 1895 | + _logger.LogWarning("库存校验API返回异常: OrderId={OrderId}, Code={Code}, Msg={Msg}", orderId, ncCode.Value, ncMsg); | |
| 1896 | + result.ErrorMessage = $"调用 ERP 库存校验接口失败:{ncMsg}"; | |
| 1897 | + return result; | |
| 1662 | 1898 | } |
| 1899 | + payload = stockResult?["data"] as JObject; | |
| 1900 | + } | |
| 1901 | + | |
| 1902 | + var stockApiOk = payload?["success"]?.ToObject<bool>() ?? false; | |
| 1903 | + if (!stockApiOk) | |
| 1904 | + { | |
| 1905 | + var errMsg = payload?["msg"]?.ToString() ?? "库存校验失败"; | |
| 1906 | + _logger.LogWarning("库存校验返回失败: OrderId={OrderId}, Msg={Msg}", orderId, errMsg); | |
| 1907 | + result.ErrorMessage = $"库存校验失败:{errMsg}"; | |
| 1908 | + return result; | |
| 1663 | 1909 | } |
| 1664 | - catch (Exception ex) | |
| 1910 | + | |
| 1911 | + var allSufficient = payload?["allSufficient"]?.ToObject<bool>() ?? false; | |
| 1912 | + if (!allSufficient) | |
| 1665 | 1913 | { |
| 1666 | - _logger.LogWarning(ex, "库存校验失败,继续使用{Djlx}: OrderId={OrderId}", finalDjlx, orderId); | |
| 1914 | + // 把具体缺货的商品名和库存信息拼出来返回给前端 | |
| 1915 | + var insufficientList = payload?["data"] as JArray; | |
| 1916 | + var insufficientMsgs = new List<string>(); | |
| 1917 | + if (insufficientList != null) | |
| 1918 | + { | |
| 1919 | + foreach (var it in insufficientList) | |
| 1920 | + { | |
| 1921 | + var suf = it?["sufficient"]?.ToObject<bool>() ?? true; | |
| 1922 | + if (suf) continue; | |
| 1923 | + // 对齐 ERP BatchCheckStock 的字段:productName / available / required | |
| 1924 | + var name = it?["productName"]?.ToString() | |
| 1925 | + ?? it?["spmc"]?.ToString() | |
| 1926 | + ?? it?["spbm"]?.ToString() | |
| 1927 | + ?? ""; | |
| 1928 | + var stock = it?["available"]?.ToString() | |
| 1929 | + ?? it?["stock"]?.ToString() | |
| 1930 | + ?? "0"; | |
| 1931 | + var need = it?["required"]?.ToString() | |
| 1932 | + ?? it?["quantity"]?.ToString() | |
| 1933 | + ?? ""; | |
| 1934 | + insufficientMsgs.Add(string.IsNullOrEmpty(need) | |
| 1935 | + ? $"{name}(库存 {stock})" | |
| 1936 | + : $"{name}(库存 {stock},需要 {need})"); | |
| 1937 | + } | |
| 1938 | + } | |
| 1939 | + var detail = insufficientMsgs.Count > 0 ? string.Join(";", insufficientMsgs) : ""; | |
| 1940 | + _logger.LogInformation("库存不足,拒绝发货: OrderId={OrderId}, 缺货明细={Detail}", orderId, detail); | |
| 1941 | + result.ErrorMessage = string.IsNullOrEmpty(detail) | |
| 1942 | + ? "商品库存不足,无法发货,请先到 ERP 补充库存后再试" | |
| 1943 | + : $"商品库存不足,无法发货:{detail}"; | |
| 1944 | + return result; | |
| 1667 | 1945 | } |
| 1946 | + | |
| 1947 | + _logger.LogInformation("库存充足,使用销售出库单: OrderId={OrderId}", orderId); | |
| 1948 | + } | |
| 1949 | + catch (Exception ex) | |
| 1950 | + { | |
| 1951 | + _logger.LogError(ex, "库存校验异常,拒绝发货: OrderId={OrderId}", orderId); | |
| 1952 | + result.ErrorMessage = $"库存校验异常:{ex.Message}"; | |
| 1953 | + return result; | |
| 1668 | 1954 | } |
| 1669 | 1955 | } |
| 1670 | 1956 | |
| ... | ... | @@ -1727,13 +2013,13 @@ public class OrderService |
| 1727 | 2013 | var defaultsData = defaultsResult?["data"]; |
| 1728 | 2014 | if (defaultsData != null) |
| 1729 | 2015 | { |
| 1730 | - if (string.IsNullOrEmpty(warehouseId)) | |
| 1731 | - warehouseId = defaultsData["mrck"]?.ToString() ?? ""; | |
| 2016 | + // 出库仓库:不再使用全局默认仓库(该默认仅供 ERP 常规单据使用)。 | |
| 2017 | + // 抖音发货单仅使用发货单 cjck 或抖音店铺设置 ck。 | |
| 1732 | 2018 | if (string.IsNullOrEmpty(defaultKh)) |
| 1733 | 2019 | defaultKh = defaultsData["mrwldw"]?.ToString() ?? ""; |
| 1734 | 2020 | if (string.IsNullOrEmpty(defaultSkzh)) |
| 1735 | 2021 | defaultSkzh = defaultsData["mrskzh"]?.ToString() ?? ""; |
| 1736 | - _logger.LogInformation("获取默认选项设置: cjck={Cjck}, kh={Kh}, skzh={Skzh}", warehouseId, defaultKh, defaultSkzh); | |
| 2022 | + _logger.LogInformation("获取默认往来单位/收款账户: kh={Kh}, skzh={Skzh}; 出库仓库={Cjck}", defaultKh, defaultSkzh, warehouseId); | |
| 1737 | 2023 | } |
| 1738 | 2024 | } |
| 1739 | 2025 | } |
| ... | ... | @@ -1769,11 +2055,46 @@ public class OrderService |
| 1769 | 2055 | payAmtFen = order.PayAmount ?? 0L; |
| 1770 | 2056 | } |
| 1771 | 2057 | |
| 2058 | + // 金额口径(与"商家实际收入"功能对齐): | |
| 2059 | + // ydje(应收)= 订单应收金额(抖音 orderAmount,汇总合并单) | |
| 2060 | + // 用户支付金额 = 抖音 payAmount(汇总合并单),不直接写入 ERP 某个字段 | |
| 2061 | + // skje(收款金额)= 发货单上的"商家实际收入"(用户可改,默认=用户支付金额) | |
| 2062 | + // ysje(优惠金额)= ydje − 用户支付金额(与原口径保持一致,不随商家实际收入联动) | |
| 1772 | 2063 | var discountFen = orderAmtFen - payAmtFen; |
| 1773 | 2064 | if (discountFen < 0) discountFen = 0; |
| 1774 | 2065 | var ydjeYuan = orderAmtFen / 100.0m; |
| 1775 | - var skjeYuan = payAmtFen / 100.0m; | |
| 1776 | - var ysjeYuan = discountFen / 100.0m; | |
| 2066 | + var payAmtYuan = payAmtFen / 100.0m; // 用户支付金额 | |
| 2067 | + var ysjeYuan = discountFen / 100.0m; // 优惠 = ydje − 用户支付金额 | |
| 2068 | + | |
| 2069 | + // 优先级:参数传入 merchantIncome > 发货单已保存的 MerchantIncome > 默认=用户支付金额 | |
| 2070 | + decimal skjeYuan; | |
| 2071 | + if (merchantIncome.HasValue) | |
| 2072 | + skjeYuan = merchantIncome.Value; | |
| 2073 | + else if (waybill?.MerchantIncome != null) | |
| 2074 | + skjeYuan = waybill.MerchantIncome.Value; | |
| 2075 | + else | |
| 2076 | + skjeYuan = payAmtYuan; | |
| 2077 | + if (skjeYuan < 0) skjeYuan = 0; | |
| 2078 | + _logger.LogInformation( | |
| 2079 | + "销售出库单金额口径: OrderId={OrderId}, ydje(应收)={Ydje}, 用户支付={Pay}, skje(商家实收)={Skje}, ysje(优惠=应收-用户支付)={Ysje}", | |
| 2080 | + orderId, ydjeYuan, payAmtYuan, skjeYuan, ysjeYuan); | |
| 2081 | + | |
| 2082 | + // 构建收款渠道明细 JSON(ERP 的"收款渠道明细"读的就是这个字段,skmx 空时 UI 会显示无数据) | |
| 2083 | + // 单账户抖音单:[{"skfs":"入账账户","skje":"49.00","skzh":"<账户id>"}] | |
| 2084 | + var skmxJson = ""; | |
| 2085 | + if (!string.IsNullOrEmpty(defaultSkzh) && skjeYuan > 0) | |
| 2086 | + { | |
| 2087 | + var skmxArr = new JArray | |
| 2088 | + { | |
| 2089 | + new JObject | |
| 2090 | + { | |
| 2091 | + ["skfs"] = "入账账户", | |
| 2092 | + ["skje"] = skjeYuan.ToString("0.00"), | |
| 2093 | + ["skzh"] = defaultSkzh | |
| 2094 | + } | |
| 2095 | + }; | |
| 2096 | + skmxJson = skmxArr.ToString(Newtonsoft.Json.Formatting.None); | |
| 2097 | + } | |
| 1777 | 2098 | |
| 1778 | 2099 | // 合并订单的抖音订单号(逗号分隔)和运单号 |
| 1779 | 2100 | var finalDouyinOrderIds = !string.IsNullOrEmpty(douyinOrderIds) ? douyinOrderIds : (order.OrderId ?? ""); |
| ... | ... | @@ -1791,6 +2112,8 @@ public class OrderService |
| 1791 | 2112 | cjck = warehouseId, |
| 1792 | 2113 | rkck = "", |
| 1793 | 2114 | jsr = "", |
| 2115 | + // 发货人:参数传入优先(当前本次提交值),否则回退 waybill 已保存值 | |
| 2116 | + fhr = !string.IsNullOrWhiteSpace(fhr) ? fhr.Trim() : (waybill?.Fhr ?? ""), | |
| 1794 | 2117 | yddh = finalTrackingNumber, |
| 1795 | 2118 | dyddh = finalDouyinOrderIds, |
| 1796 | 2119 | ydje = ydjeYuan, |
| ... | ... | @@ -1806,7 +2129,7 @@ public class OrderService |
| 1806 | 2129 | gys = "", |
| 1807 | 2130 | djzt = "已审核", |
| 1808 | 2131 | ly = "抖音订单", |
| 1809 | - skmx = "", | |
| 2132 | + skmx = skmxJson, | |
| 1810 | 2133 | wtXsckdMxList = salesOrderDetails |
| 1811 | 2134 | }; |
| 1812 | 2135 | |
| ... | ... | @@ -2153,6 +2476,38 @@ public class OrderService |
| 2153 | 2476 | } |
| 2154 | 2477 | |
| 2155 | 2478 | /// <summary> |
| 2479 | + /// 删除 ERP 侧的销售/预售出库单(用于抖音端编辑发货单时,先清掉旧单再生成新单) | |
| 2480 | + /// </summary> | |
| 2481 | + /// <param name="salesOrderId">ERP 销售出库单 ID</param> | |
| 2482 | + /// <returns>是否删除成功;已不存在、已审核或关联退货单等情况会返回 false 并写入日志</returns> | |
| 2483 | + public async Task<bool> DeleteErpSalesOrderAsync(string salesOrderId) | |
| 2484 | + { | |
| 2485 | + if (string.IsNullOrWhiteSpace(salesOrderId)) return false; | |
| 2486 | + try | |
| 2487 | + { | |
| 2488 | + var url = $"{_erpApiConfig.BaseUrl}/api/Extend/wtxsckd/{salesOrderId}"; | |
| 2489 | + var request = await CreateAuthenticatedRequestAsync(HttpMethod.Delete, url); | |
| 2490 | + var response = await _httpClient.SendAsync(request); | |
| 2491 | + var body = await response.Content.ReadAsStringAsync(); | |
| 2492 | + | |
| 2493 | + if (response.IsSuccessStatusCode) | |
| 2494 | + { | |
| 2495 | + _logger.LogInformation("已删除 ERP 销售出库单: SalesOrderId={SalesOrderId}, Resp={Body}", salesOrderId, body); | |
| 2496 | + return true; | |
| 2497 | + } | |
| 2498 | + | |
| 2499 | + _logger.LogWarning("删除 ERP 销售出库单失败: SalesOrderId={SalesOrderId}, Status={Status}, Body={Body}", | |
| 2500 | + salesOrderId, response.StatusCode, body); | |
| 2501 | + return false; | |
| 2502 | + } | |
| 2503 | + catch (Exception ex) | |
| 2504 | + { | |
| 2505 | + _logger.LogError(ex, "删除 ERP 销售出库单异常: SalesOrderId={SalesOrderId}", salesOrderId); | |
| 2506 | + return false; | |
| 2507 | + } | |
| 2508 | + } | |
| 2509 | + | |
| 2510 | + /// <summary> | |
| 2156 | 2511 | /// 创建带认证的HTTP请求 |
| 2157 | 2512 | /// </summary> |
| 2158 | 2513 | public async Task<HttpRequestMessage> CreateAuthenticatedRequestAsync(HttpMethod method, string url, HttpContent? content = null) | ... | ... |
Antis.Erp.Plat/douyin/DouyinLogistics.API/Services/ShopConfigProvider.cs
0 → 100644
| 1 | +using DouyinLogistics.API.Models; | |
| 2 | +using System.Net.Http.Headers; | |
| 3 | +using System.Text.Json; | |
| 4 | + | |
| 5 | +namespace DouyinLogistics.API.Services; | |
| 6 | + | |
| 7 | +/// <summary> | |
| 8 | +/// 从 ERP(WtDyDpsz/internal/all)拉取抖音店铺配置。 | |
| 9 | +/// 如果 ERP 不可用或未配置任何店铺,则回退到 appsettings.json 中的 DouyinShops。 | |
| 10 | +/// </summary> | |
| 11 | +public class ShopConfigProvider | |
| 12 | +{ | |
| 13 | + private readonly IHttpClientFactory _httpClientFactory; | |
| 14 | + private readonly ILogger<ShopConfigProvider> _logger; | |
| 15 | + private readonly ErpApiConfig _erpApiConfig; | |
| 16 | + private readonly List<DouyinConfig> _fallbackConfigs; | |
| 17 | + | |
| 18 | + public ShopConfigProvider( | |
| 19 | + IHttpClientFactory httpClientFactory, | |
| 20 | + ILogger<ShopConfigProvider> logger, | |
| 21 | + ErpApiConfig erpApiConfig, | |
| 22 | + IEnumerable<DouyinConfig> fallbackConfigs) | |
| 23 | + { | |
| 24 | + _httpClientFactory = httpClientFactory; | |
| 25 | + _logger = logger; | |
| 26 | + _erpApiConfig = erpApiConfig; | |
| 27 | + _fallbackConfigs = fallbackConfigs?.ToList() ?? new List<DouyinConfig>(); | |
| 28 | + } | |
| 29 | + | |
| 30 | + /// <summary> | |
| 31 | + /// 拉取最新店铺配置;ERP 拉不到时自动回退 appsettings。 | |
| 32 | + /// </summary> | |
| 33 | + public async Task<List<DouyinConfig>> FetchAllAsync(CancellationToken ct = default) | |
| 34 | + { | |
| 35 | + if (string.IsNullOrWhiteSpace(_erpApiConfig?.BaseUrl)) | |
| 36 | + { | |
| 37 | + _logger.LogWarning("未配置 ErpApi.BaseUrl,使用 appsettings.json 中的 DouyinShops 作为兜底"); | |
| 38 | + return _fallbackConfigs; | |
| 39 | + } | |
| 40 | + | |
| 41 | + try | |
| 42 | + { | |
| 43 | + var client = _httpClientFactory.CreateClient(); | |
| 44 | + client.Timeout = TimeSpan.FromSeconds(10); | |
| 45 | + if (!client.DefaultRequestHeaders.Contains("User-Agent")) | |
| 46 | + client.DefaultRequestHeaders.Add("User-Agent", "DouyinLogistics-API/1.0"); | |
| 47 | + var url = $"{_erpApiConfig.BaseUrl.TrimEnd('/')}/api/Extend/WtDyDpsz/internal/all"; | |
| 48 | + using var req = new HttpRequestMessage(HttpMethod.Get, url); | |
| 49 | + req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); | |
| 50 | + var resp = await client.SendAsync(req, ct); | |
| 51 | + var body = await resp.Content.ReadAsStringAsync(ct); | |
| 52 | + if (!resp.IsSuccessStatusCode) | |
| 53 | + { | |
| 54 | + _logger.LogWarning("从 ERP 拉取店铺配置失败: {Status} {Body}", resp.StatusCode, body); | |
| 55 | + return _fallbackConfigs; | |
| 56 | + } | |
| 57 | + | |
| 58 | + var list = ParseShops(body); | |
| 59 | + if (list.Count == 0) | |
| 60 | + { | |
| 61 | + _logger.LogWarning("ERP 返回的店铺列表为空,回退到 appsettings"); | |
| 62 | + return _fallbackConfigs; | |
| 63 | + } | |
| 64 | + _logger.LogInformation("✅ 已从 ERP 拉取 {Count} 个抖音店铺配置", list.Count); | |
| 65 | + return list; | |
| 66 | + } | |
| 67 | + catch (Exception ex) | |
| 68 | + { | |
| 69 | + _logger.LogError(ex, "从 ERP 拉取店铺配置异常,回退到 appsettings"); | |
| 70 | + return _fallbackConfigs; | |
| 71 | + } | |
| 72 | + } | |
| 73 | + | |
| 74 | + private static List<DouyinConfig> ParseShops(string json) | |
| 75 | + { | |
| 76 | + var result = new List<DouyinConfig>(); | |
| 77 | + if (string.IsNullOrWhiteSpace(json)) return result; | |
| 78 | + using var doc = JsonDocument.Parse(json); | |
| 79 | + // NCC 框架响应统一为 { code, data, msg, ... } | |
| 80 | + JsonElement array; | |
| 81 | + if (doc.RootElement.ValueKind == JsonValueKind.Array) | |
| 82 | + { | |
| 83 | + array = doc.RootElement; | |
| 84 | + } | |
| 85 | + else if (doc.RootElement.TryGetProperty("data", out var dataElem)) | |
| 86 | + { | |
| 87 | + if (dataElem.ValueKind == JsonValueKind.Array) | |
| 88 | + array = dataElem; | |
| 89 | + else | |
| 90 | + return result; | |
| 91 | + } | |
| 92 | + else | |
| 93 | + { | |
| 94 | + return result; | |
| 95 | + } | |
| 96 | + | |
| 97 | + foreach (var item in array.EnumerateArray()) | |
| 98 | + { | |
| 99 | + var cfg = new DouyinConfig | |
| 100 | + { | |
| 101 | + ShopId = GetLong(item, "shopId"), | |
| 102 | + ShopName = GetString(item, "shopName"), | |
| 103 | + AppKey = GetString(item, "appKey"), | |
| 104 | + AppSecret = GetString(item, "appSecret"), | |
| 105 | + CallbackUrl = GetString(item, "callbackUrl"), | |
| 106 | + ApiBaseUrl = EmptyFallback(GetString(item, "apiBaseUrl"), "https://openapi-fxg.jinritemai.com"), | |
| 107 | + SyncDays = (int)GetLong(item, "syncDays", 30), | |
| 108 | + SenderName = GetString(item, "senderName"), | |
| 109 | + SenderPhone = GetString(item, "senderPhone"), | |
| 110 | + SenderAddress = GetString(item, "senderAddress"), | |
| 111 | + SenderProvince = GetString(item, "senderProvince"), | |
| 112 | + SenderCity = GetString(item, "senderCity"), | |
| 113 | + SenderDistrict = GetString(item, "senderDistrict"), | |
| 114 | + SenderStreet = GetString(item, "senderStreet"), | |
| 115 | + }; | |
| 116 | + if (cfg.ShopId > 0 && !string.IsNullOrEmpty(cfg.AppKey) && !string.IsNullOrEmpty(cfg.AppSecret)) | |
| 117 | + { | |
| 118 | + result.Add(cfg); | |
| 119 | + } | |
| 120 | + } | |
| 121 | + return result; | |
| 122 | + } | |
| 123 | + | |
| 124 | + private static string GetString(JsonElement el, string name) | |
| 125 | + { | |
| 126 | + if (el.TryGetProperty(name, out var prop) && prop.ValueKind == JsonValueKind.String) | |
| 127 | + return prop.GetString() ?? string.Empty; | |
| 128 | + return string.Empty; | |
| 129 | + } | |
| 130 | + | |
| 131 | + private static long GetLong(JsonElement el, string name, long def = 0) | |
| 132 | + { | |
| 133 | + if (!el.TryGetProperty(name, out var prop)) return def; | |
| 134 | + return prop.ValueKind switch | |
| 135 | + { | |
| 136 | + JsonValueKind.Number => prop.TryGetInt64(out var v) ? v : def, | |
| 137 | + JsonValueKind.String => long.TryParse(prop.GetString(), out var v) ? v : def, | |
| 138 | + _ => def | |
| 139 | + }; | |
| 140 | + } | |
| 141 | + | |
| 142 | + private static string EmptyFallback(string s, string def) => | |
| 143 | + string.IsNullOrWhiteSpace(s) ? def : s; | |
| 144 | +} | ... | ... |
Antis.Erp.Plat/douyin/DouyinLogistics.API/appsettings.json
| ... | ... | @@ -9,24 +9,8 @@ |
| 9 | 9 | "ConnectionStrings": { |
| 10 | 10 | "DefaultConnection": "Server=rm-bp19ohrgc6111ynzh1o.mysql.rds.aliyuncs.com;Port=3306;Database=ncc_wutong;Uid=netteam;Pwd=netteam;CharSet=utf8mb4;SslMode=None;AllowPublicKeyRetrieval=true;" |
| 11 | 11 | }, |
| 12 | - "DouyinShops": [ | |
| 13 | - { | |
| 14 | - "ShopName": "梧桐官方店", | |
| 15 | - "SyncDays": 30, | |
| 16 | - "AppKey": "7463313337959335475", | |
| 17 | - "AppSecret": "648ddb67-b3ba-48a3-905c-ea761e69a87e", | |
| 18 | - "CallbackUrl": "http://localhost:5070/api/auth/callback", | |
| 19 | - "ApiBaseUrl": "https://openapi-fxg.jinritemai.com", | |
| 20 | - "ShopId": 37714425, | |
| 21 | - "SenderName": "PongGame", | |
| 22 | - "SenderPhone": "18014801756", | |
| 23 | - "SenderAddress": "浦洲路39号沿海创中心A区301室", | |
| 24 | - "SenderProvince": "江苏省", | |
| 25 | - "SenderCity": "南京市", | |
| 26 | - "SenderDistrict": "浦口区", | |
| 27 | - "SenderStreet": "沿江街道" | |
| 28 | - } | |
| 29 | - ], | |
| 12 | + "__DouyinShops_Readme": "抖音店铺配置已迁移至 ERP「抖音店铺设置」(wt_dy_dpsz),启动时从 ERP 拉取。下面的 DouyinShops 仅作为 ERP 不可用时的兜底,生产环境可留空。", | |
| 13 | + "DouyinShops": [], | |
| 30 | 14 | "Sf": { |
| 31 | 15 | "CustomerCode": "your_customer_code", |
| 32 | 16 | "CheckWord": "your_check_word", | ... | ... |
Antis.Erp.Plat/douyin/frontend/src/api/order.ts
| ... | ... | @@ -36,6 +36,10 @@ export interface Order { |
| 36 | 36 | hasSubmittedShipmentForm?: boolean |
| 37 | 37 | /** 是否“已发货-未提交发货单” */ |
| 38 | 38 | isPendingShipmentForm?: boolean |
| 39 | + /** 是否已被用户手动从合并组拆分出来(true 表示不再参与自动合并) */ | |
| 40 | + noMerge?: boolean | |
| 41 | + /** 是否可以"恢复合并":noMerge=true 且同买家+同地址下仍有其他待发货订单 */ | |
| 42 | + canUnsplit?: boolean | |
| 39 | 43 | receiverName: string |
| 40 | 44 | receiverPhone: string |
| 41 | 45 | receiverAddress: string |
| ... | ... | @@ -195,6 +199,16 @@ export const getMergedOrderDetail = (ids: number[]) => { |
| 195 | 199 | return api.get('/orders/detail/merged', { params: { ids: ids.join(',') } }) |
| 196 | 200 | } |
| 197 | 201 | |
| 202 | +// 拆分合并单:把这组订单从自动合并组中拆出来 | |
| 203 | +export const splitMergedOrders = (orderIds: number[]) => { | |
| 204 | + return api.post('/orders/split', { orderIds }) | |
| 205 | +} | |
| 206 | + | |
| 207 | +// 恢复合并:取消"拆分"标记 | |
| 208 | +export const unsplitOrders = (orderIds: number[]) => { | |
| 209 | + return api.post('/orders/unsplit', { orderIds }) | |
| 210 | +} | |
| 211 | + | |
| 198 | 212 | // 查询ERP商品列表(pl:单个品类 F_Id;ERP 商品可挂多品类逗号存储,后端按「包含该 ID」匹配) |
| 199 | 213 | export const searchProducts = (keyword?: string, pageIndex = 1, pageSize = 20, pl?: string) => { |
| 200 | 214 | const params: any = { pageIndex, pageSize } |
| ... | ... | @@ -251,6 +265,11 @@ export const getPaymentAccounts = () => { |
| 251 | 265 | return api.get('/orders/payment-accounts') |
| 252 | 266 | } |
| 253 | 267 | |
| 268 | +// 获取 ERP 用户列表(用于"发货人"下拉) | |
| 269 | +export const getUsers = () => { | |
| 270 | + return api.get('/orders/users') | |
| 271 | +} | |
| 272 | + | |
| 254 | 273 | // 获取抖音店铺设置(往来单位/收款账户) |
| 255 | 274 | export const getShopSettings = (shopId: number) => { |
| 256 | 275 | return api.get(`/orders/shop-settings/${shopId}`) | ... | ... |
Antis.Erp.Plat/douyin/frontend/src/components/SerialNumberSelect.vue
| ... | ... | @@ -16,27 +16,22 @@ |
| 16 | 16 | <div class="serial-number-select"> |
| 17 | 17 | <!-- 搜索条件 --> |
| 18 | 18 | <el-form :inline="true" :model="searchForm" class="search-form"> |
| 19 | - <el-form-item label="商品编号"> | |
| 20 | - <el-input v-model="searchForm.productCode" placeholder="请输入商品编号" readonly></el-input> | |
| 21 | - </el-form-item> | |
| 22 | 19 | <el-form-item label="仓库"> |
| 23 | - <el-select | |
| 24 | - v-model="searchForm.warehouse" | |
| 25 | - placeholder="请选择仓库" | |
| 26 | - clearable | |
| 27 | - @change="(val: any) => console.log('仓库下拉框值变化:', val, '当前searchForm.warehouse:', searchForm.warehouse)" | |
| 28 | - @visible-change="(visible: boolean) => visible && console.log('下拉框打开,当前值:', searchForm.warehouse)" | |
| 29 | - > | |
| 30 | - <el-option | |
| 31 | - v-for="item in warehouseOptions" | |
| 32 | - :key="String(item.id || item.F_Id)" | |
| 33 | - :label="item.mdmc || item.F_mdmc || item.name" | |
| 34 | - :value="String(item.id || item.F_Id)" | |
| 35 | - ></el-option> | |
| 36 | - </el-select> | |
| 20 | + <!-- 仓库以发货单选择的出库仓库为准,这里只展示不允许修改 --> | |
| 21 | + <el-input | |
| 22 | + :model-value="currentWarehouseName" | |
| 23 | + placeholder="请在发货单中选择出库仓库" | |
| 24 | + readonly | |
| 25 | + style="width: 200px;" | |
| 26 | + ></el-input> | |
| 37 | 27 | </el-form-item> |
| 38 | 28 | <el-form-item label="序列号"> |
| 39 | - <el-input v-model="searchForm.serialNumber" placeholder="支持模糊查询"></el-input> | |
| 29 | + <el-input | |
| 30 | + v-model="searchForm.serialNumber" | |
| 31 | + placeholder="支持模糊查询" | |
| 32 | + clearable | |
| 33 | + @keyup.enter="searchSerialNumbers" | |
| 34 | + ></el-input> | |
| 40 | 35 | </el-form-item> |
| 41 | 36 | <el-form-item> |
| 42 | 37 | <el-button type="primary" @click="searchSerialNumbers">查询</el-button> |
| ... | ... | @@ -44,18 +39,24 @@ |
| 44 | 39 | </el-form-item> |
| 45 | 40 | </el-form> |
| 46 | 41 | |
| 47 | - <!-- 序列号列表 --> | |
| 48 | - <el-table | |
| 42 | + <!-- 选择数量提示:需要 N 个、已选 M 个 --> | |
| 43 | + <div class="count-tip" v-if="maxCount > 0"> | |
| 44 | + 需选择 <span class="hl">{{ maxCount }}</span> 个,已选 | |
| 45 | + <span class="hl" :class="{ 'over': selectedSerialNumbers.length > maxCount }">{{ selectedSerialNumbers.length }}</span> 个 | |
| 46 | + </div> | |
| 47 | + | |
| 48 | + <!-- 序列号列表(前端分页,防止 1000+ 一次性渲染) --> | |
| 49 | + <el-table | |
| 49 | 50 | ref="serialTable" |
| 50 | - :data="serialNumberList" | |
| 51 | - size="small" | |
| 51 | + :data="pagedList" | |
| 52 | + size="small" | |
| 53 | + row-key="serialNumber" | |
| 52 | 54 | @selection-change="handleSelectionChange" |
| 53 | 55 | v-loading="loading" |
| 54 | 56 | > |
| 55 | - <el-table-column type="selection" width="55"></el-table-column> | |
| 56 | - <el-table-column prop="serialNumber" label="序列号" width="180"></el-table-column> | |
| 57 | - <el-table-column prop="productCode" label="商品编号" width="120"></el-table-column> | |
| 58 | - <el-table-column prop="productName" label="商品名称" width="150"></el-table-column> | |
| 57 | + <el-table-column type="selection" width="55" reserve-selection :selectable="isRowSelectable"></el-table-column> | |
| 58 | + <el-table-column prop="serialNumber" label="序列号" min-width="220" show-overflow-tooltip></el-table-column> | |
| 59 | + <el-table-column prop="productName" label="商品名称" min-width="220" show-overflow-tooltip></el-table-column> | |
| 59 | 60 | <el-table-column prop="warehouse" label="仓库" width="120"> |
| 60 | 61 | <template #default="{ row }"> |
| 61 | 62 | {{ getWarehouseName(row.warehouse) }} |
| ... | ... | @@ -75,12 +76,25 @@ |
| 75 | 76 | </el-table-column> |
| 76 | 77 | </el-table> |
| 77 | 78 | |
| 79 | + <!-- 分页 --> | |
| 80 | + <div class="pagination-wrap" v-if="serialNumberList.length > pageSize"> | |
| 81 | + <el-pagination | |
| 82 | + v-model:current-page="currentPage" | |
| 83 | + v-model:page-size="pageSize" | |
| 84 | + :total="serialNumberList.length" | |
| 85 | + :page-sizes="[20, 50, 100, 200]" | |
| 86 | + layout="total, sizes, prev, pager, next, jumper" | |
| 87 | + background | |
| 88 | + small | |
| 89 | + /> | |
| 90 | + </div> | |
| 91 | + | |
| 78 | 92 | <!-- 已选择的序列号 --> |
| 79 | 93 | <div class="selected-serial-numbers" v-if="selectedSerialNumbers.length > 0"> |
| 80 | 94 | <h4>已选择的序列号 ({{ selectedSerialNumbers.length }}个):</h4> |
| 81 | 95 | <div class="selected-list"> |
| 82 | - <el-tag | |
| 83 | - v-for="serialNumber in selectedSerialNumbers" | |
| 96 | + <el-tag | |
| 97 | + v-for="serialNumber in selectedSerialNumbers" | |
| 84 | 98 | :key="serialNumber" |
| 85 | 99 | closable |
| 86 | 100 | @close="removeSelected(serialNumber)" |
| ... | ... | @@ -95,19 +109,19 @@ |
| 95 | 109 | <template #footer> |
| 96 | 110 | <el-button @click="handleClose">取 消</el-button> |
| 97 | 111 | <el-button type="primary" @click="confirmSelection"> |
| 98 | - 确 定 ({{ selectedSerialNumbers.length }}) | |
| 112 | + 确 定 ({{ selectedSerialNumbers.length }}{{ maxCount > 0 ? ` / ${maxCount}` : '' }}) | |
| 99 | 113 | </el-button> |
| 100 | 114 | </template> |
| 101 | 115 | </el-dialog> |
| 102 | 116 | </template> |
| 103 | 117 | |
| 104 | 118 | <script setup lang="ts"> |
| 105 | -import { ref, nextTick, watch } from 'vue' | |
| 119 | +import { ref, nextTick, computed } from 'vue' | |
| 106 | 120 | import axios from 'axios' |
| 107 | 121 | import { ElMessage } from 'element-plus' |
| 108 | 122 | |
| 109 | 123 | // 使用Douyin API(5070端口),避免CORS问题 |
| 110 | -const apiBaseURL = import.meta.env.VITE_API_BASE_URL || | |
| 124 | +const apiBaseURL = import.meta.env.VITE_API_BASE_URL || | |
| 111 | 125 | (import.meta.env.DEV ? 'http://localhost:5070/api' : '/api') |
| 112 | 126 | |
| 113 | 127 | const api = axios.create({ |
| ... | ... | @@ -117,6 +131,7 @@ const api = axios.create({ |
| 117 | 131 | |
| 118 | 132 | const visible = ref(false) |
| 119 | 133 | const loading = ref(false) |
| 134 | +// 仓库由发货单页面传入,不在弹窗里让用户改;productCode 只用于查询参数,不展示 | |
| 120 | 135 | const searchForm = ref({ |
| 121 | 136 | productCode: '', |
| 122 | 137 | warehouse: '', |
| ... | ... | @@ -128,216 +143,80 @@ const warehouseOptions = ref<any[]>([]) |
| 128 | 143 | const currentProductCode = ref('') |
| 129 | 144 | const currentWarehouse = ref('') |
| 130 | 145 | const currentDocumentType = ref('') |
| 146 | +// 商品清单里的需求数量,0 表示不限制 | |
| 147 | +const maxCount = ref(0) | |
| 131 | 148 | |
| 132 | -// 获取仓库选项 | |
| 149 | +// 分页(纯前端分页,后端一次性返回全量) | |
| 150 | +const currentPage = ref(1) | |
| 151 | +const pageSize = ref(50) | |
| 152 | +const pagedList = computed(() => { | |
| 153 | + const start = (currentPage.value - 1) * pageSize.value | |
| 154 | + return serialNumberList.value.slice(start, start + pageSize.value) | |
| 155 | +}) | |
| 156 | + | |
| 157 | +// 未勾选且已选满时,禁止再勾(已勾选的不受影响,允许取消) | |
| 158 | +const isRowSelectable = (row: any) => { | |
| 159 | + if (!maxCount.value) return true | |
| 160 | + const sn = String(row?.serialNumber || '').trim() | |
| 161 | + if (selectedSerialNumbers.value.includes(sn)) return true | |
| 162 | + return selectedSerialNumbers.value.length < maxCount.value | |
| 163 | +} | |
| 164 | + | |
| 165 | +const currentWarehouseName = computed(() => { | |
| 166 | + const id = String(currentWarehouse.value || '').trim() | |
| 167 | + if (!id) return '' | |
| 168 | + const match = warehouseOptions.value.find((item: any) => | |
| 169 | + String(item.id || item.F_Id || item.Id || '').trim() === id | |
| 170 | + ) | |
| 171 | + return match ? (match.mdmc || match.F_mdmc || match.name || id) : id | |
| 172 | +}) | |
| 173 | + | |
| 174 | +// 获取仓库选项(仅用于把仓库 ID 解析成名称展示) | |
| 133 | 175 | const getWarehouseOptions = async () => { |
| 134 | 176 | try { |
| 135 | - // 使用Douyin API代理接口,避免CORS问题 | |
| 136 | 177 | const response = await api.get('/orders/warehouses') |
| 137 | - console.log('获取仓库选项 - 响应:', response.data) | |
| 138 | 178 | if (response.data && response.data.code === 200) { |
| 139 | 179 | warehouseOptions.value = response.data.data || [] |
| 140 | - console.log('获取到仓库选项数量:', warehouseOptions.value.length, warehouseOptions.value) | |
| 141 | 180 | } else if (response.data && Array.isArray(response.data)) { |
| 142 | 181 | warehouseOptions.value = response.data |
| 143 | 182 | } else { |
| 144 | - console.warn('仓库选项格式不正确:', response.data) | |
| 145 | 183 | warehouseOptions.value = [] |
| 146 | 184 | } |
| 147 | 185 | } catch (error) { |
| 148 | 186 | console.error('获取仓库选项失败:', error) |
| 149 | - // 如果API调用失败,尝试使用空数组 | |
| 150 | 187 | warehouseOptions.value = [] |
| 151 | 188 | } |
| 152 | 189 | } |
| 153 | 190 | |
| 154 | -// 打开弹窗 | |
| 155 | -const open = async (productCode: string, warehouse: string = '', selectedSerialNumbersList: string[] = [], documentType: string = '发货单') => { | |
| 191 | +// 打开弹窗:productCode = 发货行商品(F_Id 或 spbm 或抖音SKU),warehouse = 发货单已选的出库仓库 ID | |
| 192 | +// max = 商品清单中的需求数量(≤0 表示不限制) | |
| 193 | +const open = async (productCode: string, warehouse: string = '', selectedSerialNumbersList: string[] = [], documentType: string = '发货单', max: number = 0) => { | |
| 156 | 194 | visible.value = true |
| 157 | 195 | currentProductCode.value = productCode |
| 158 | - currentWarehouse.value = warehouse | |
| 196 | + currentWarehouse.value = warehouse || '' | |
| 159 | 197 | currentDocumentType.value = documentType |
| 160 | - searchForm.value.productCode = productCode // 先设置为原始值 | |
| 198 | + maxCount.value = Math.max(0, Number(max) || 0) | |
| 199 | + searchForm.value.productCode = productCode | |
| 200 | + searchForm.value.warehouse = warehouse || '' | |
| 161 | 201 | searchForm.value.serialNumber = '' |
| 162 | 202 | selectedSerialNumbers.value = [...selectedSerialNumbersList] |
| 163 | - | |
| 164 | - console.log('打开序列号选择弹窗 - 商品编码:', productCode, '仓库:', warehouse, '仓库选项数量:', warehouseOptions.value.length) | |
| 165 | - | |
| 166 | - // 确保仓库选项已加载(每次打开弹窗都重新加载,确保数据最新) | |
| 167 | - await getWarehouseOptions() | |
| 168 | - | |
| 169 | - // 设置仓库(在仓库选项加载后设置,确保能正确匹配) | |
| 170 | - // 使用nextTick确保仓库选项已完全渲染 | |
| 171 | - await nextTick() | |
| 172 | - | |
| 173 | - if (warehouse) { | |
| 174 | - console.log('开始匹配仓库 - 传入值:', warehouse, '类型:', typeof warehouse) | |
| 175 | - console.log('仓库选项数量:', warehouseOptions.value.length) | |
| 176 | - | |
| 177 | - // 尝试匹配仓库ID(支持多种字段名和类型转换) | |
| 178 | - const warehouseStr = String(warehouse).trim() | |
| 179 | - let matchedWarehouse = null | |
| 180 | - let matchedId = null | |
| 181 | - | |
| 182 | - // 遍历所有仓库选项,尝试匹配 | |
| 183 | - for (const item of warehouseOptions.value) { | |
| 184 | - const itemId = item.id || item.F_Id || item.Id || '' | |
| 185 | - const itemIdStr = String(itemId).trim() | |
| 186 | - | |
| 187 | - // 尝试多种匹配方式 | |
| 188 | - if (itemIdStr === warehouseStr || | |
| 189 | - item.id === warehouse || | |
| 190 | - item.F_Id === warehouse || | |
| 191 | - item.Id === warehouse || | |
| 192 | - String(item.id) === String(warehouse) || | |
| 193 | - String(item.F_Id) === String(warehouse)) { | |
| 194 | - matchedWarehouse = item | |
| 195 | - matchedId = item.id || item.F_Id || warehouse | |
| 196 | - console.log('✅ 找到匹配的仓库:', { | |
| 197 | - 匹配项: item, | |
| 198 | - 使用的ID: matchedId, | |
| 199 | - 原始传入值: warehouse | |
| 200 | - }) | |
| 201 | - break | |
| 202 | - } | |
| 203 | - } | |
| 204 | - | |
| 205 | - if (matchedWarehouse && matchedId) { | |
| 206 | - // 使用与el-option的value相同的格式:item.id || item.F_Id | |
| 207 | - // 确保值的类型和格式与el-option的value完全一致 | |
| 208 | - const finalWarehouseId = matchedWarehouse.id || matchedWarehouse.F_Id || matchedId | |
| 209 | - console.log('✅ 成功匹配仓库,准备设置searchForm.warehouse为:', finalWarehouseId) | |
| 210 | - console.log('匹配到的仓库对象:', matchedWarehouse) | |
| 211 | - console.log('el-option的value格式应该是:', matchedWarehouse.id || matchedWarehouse.F_Id) | |
| 212 | - | |
| 213 | - // 直接设置仓库值,确保类型与el-option的value一致(字符串) | |
| 214 | - const warehouseValue = String(finalWarehouseId) | |
| 215 | - | |
| 216 | - // 先清空,再设置,确保Vue能检测到变化 | |
| 217 | - searchForm.value.warehouse = '' | |
| 218 | - await nextTick() | |
| 219 | - | |
| 220 | - // 设置新值 | |
| 221 | - searchForm.value.warehouse = warehouseValue | |
| 222 | - console.log('设置后的searchForm.warehouse值:', searchForm.value.warehouse) | |
| 223 | - console.log('设置值的类型:', typeof searchForm.value.warehouse) | |
| 224 | - console.log('仓库选项数量:', warehouseOptions.value.length) | |
| 225 | - console.log('仓库选项的value列表:', warehouseOptions.value.map((item: any) => String(item.id || item.F_Id))) | |
| 226 | - | |
| 227 | - // 使用nextTick确保Vue已更新响应式数据和DOM | |
| 228 | - await nextTick() | |
| 229 | - console.log('nextTick后的searchForm.warehouse值:', searchForm.value.warehouse) | |
| 230 | - | |
| 231 | - // 再次使用nextTick,确保el-select组件已更新 | |
| 232 | - await nextTick() | |
| 233 | - console.log('第二次nextTick后的searchForm.warehouse值:', searchForm.value.warehouse) | |
| 234 | - | |
| 235 | - // 验证值是否在选项中(使用字符串比较) | |
| 236 | - const verifyValue = String(finalWarehouseId) | |
| 237 | - const foundOption = warehouseOptions.value.find((item: any) => { | |
| 238 | - const optionValue = String(item.id || item.F_Id) | |
| 239 | - return optionValue === verifyValue | |
| 240 | - }) | |
| 241 | - if (foundOption) { | |
| 242 | - console.log('✅ 最终验证:仓库值在选项中,匹配的选项:', foundOption) | |
| 243 | - } else { | |
| 244 | - console.error('❌ 最终验证失败:仓库值不在选项中') | |
| 245 | - } | |
| 246 | - } else { | |
| 247 | - // 如果没有匹配到,直接使用传入的仓库值(可能是仓库ID) | |
| 248 | - // 但需要确保格式与el-option的value一致 | |
| 249 | - searchForm.value.warehouse = warehouse | |
| 250 | - console.log('⚠️ 未在仓库选项中找到匹配的仓库,直接使用传入值:', warehouse) | |
| 251 | - console.log('仓库选项列表:', warehouseOptions.value.map((item: any) => ({ | |
| 252 | - id: item.id, | |
| 253 | - F_Id: item.F_Id, | |
| 254 | - Id: item.Id, | |
| 255 | - label: item.mdmc || item.F_mdmc || item.name, | |
| 256 | - 'id类型': typeof item.id, | |
| 257 | - 'F_Id类型': typeof item.F_Id | |
| 258 | - }))) | |
| 259 | - } | |
| 260 | - } else { | |
| 261 | - searchForm.value.warehouse = '' | |
| 262 | - console.log('⚠️ 未传入仓库参数') | |
| 263 | - } | |
| 264 | - | |
| 265 | - // 如果productCode是长数字(可能是抖音SKU编码),先尝试转换为ERP商品ID(F_Id) | |
| 266 | - // 这样输入框和表格中显示的就是F_Id,而不是原始的SKU编码 | |
| 267 | - if (productCode && productCode.length > 10 && /^\d+$/.test(productCode)) { | |
| 268 | - try { | |
| 269 | - console.log('检测到可能是抖音SKU编码,尝试转换为ERP商品ID:', productCode) | |
| 270 | - // 调用后端API获取商品信息(通过SKU编码) | |
| 271 | - const response = await api.get('/orders/products/info', { | |
| 272 | - params: { skuId: productCode } | |
| 273 | - }) | |
| 274 | - | |
| 275 | - if (response.data && response.data.code === 200 && response.data.data && response.data.data.id) { | |
| 276 | - const productId = response.data.data.id | |
| 277 | - console.log('成功将抖音SKU编码转换为ERP商品ID:', productCode, '->', productId) | |
| 278 | - // 更新输入框中的商品编号为F_Id | |
| 279 | - searchForm.value.productCode = productId | |
| 280 | - currentProductCode.value = productId | |
| 281 | - } else { | |
| 282 | - console.warn('未找到抖音SKU编码对应的ERP商品ID,使用原值:', productCode) | |
| 283 | - } | |
| 284 | - } catch (error) { | |
| 285 | - console.warn('转换抖音SKU编码失败,使用原值:', productCode, error) | |
| 286 | - } | |
| 203 | + currentPage.value = 1 | |
| 204 | + | |
| 205 | + console.log('[序列号弹窗] 打开: productCode=', productCode, ', warehouse=', warehouse, ', max=', max, ', preSelected=', selectedSerialNumbersList) | |
| 206 | + | |
| 207 | + // 仓库名展示需要仓库选项列表 | |
| 208 | + if (warehouseOptions.value.length === 0) { | |
| 209 | + await getWarehouseOptions() | |
| 287 | 210 | } |
| 288 | - | |
| 289 | - // 再次检查仓库设置(在商品编码转换后,确保仓库值正确) | |
| 290 | - // 使用nextTick确保仓库选项已完全加载和DOM已更新 | |
| 211 | + | |
| 212 | + // 清空上次残留的选择状态,避免 reserve-selection 跨次保留 | |
| 213 | + ;(serialTable.value as any)?.clearSelection?.() | |
| 214 | + | |
| 291 | 215 | await nextTick() |
| 292 | - console.log('最终检查 - 仓库值:', searchForm.value.warehouse, '仓库选项数量:', warehouseOptions.value.length) | |
| 293 | - | |
| 294 | - // 如果仓库值已设置,验证它是否在选项中 | |
| 295 | - if (searchForm.value.warehouse && warehouseOptions.value.length > 0) { | |
| 296 | - const finalWarehouse = String(searchForm.value.warehouse).trim() | |
| 297 | - const found = warehouseOptions.value.find((item: any) => { | |
| 298 | - const itemId = String(item.id || item.F_Id || '').trim() | |
| 299 | - return itemId === finalWarehouse | |
| 300 | - }) | |
| 301 | - | |
| 302 | - if (!found) { | |
| 303 | - console.warn('⚠️ 最终检查:仓库值未在选项中找到,当前值:', finalWarehouse) | |
| 304 | - console.log('仓库选项列表:', warehouseOptions.value.map((item: any) => ({ | |
| 305 | - id: item.id, | |
| 306 | - F_Id: item.F_Id, | |
| 307 | - label: item.mdmc || item.F_mdmc || item.name, | |
| 308 | - 'value格式': item.id || item.F_Id | |
| 309 | - }))) | |
| 310 | - | |
| 311 | - // 尝试重新匹配原始传入的warehouse值 | |
| 312 | - if (warehouse) { | |
| 313 | - const warehouseStr = String(warehouse).trim() | |
| 314 | - const retryMatch = warehouseOptions.value.find((item: any) => { | |
| 315 | - const itemId = String(item.id || item.F_Id || '').trim() | |
| 316 | - return itemId === warehouseStr | |
| 317 | - }) | |
| 318 | - | |
| 319 | - if (retryMatch) { | |
| 320 | - const retryId = retryMatch.id || retryMatch.F_Id || warehouse | |
| 321 | - searchForm.value.warehouse = retryId | |
| 322 | - console.log('✅ 重新匹配成功,设置仓库为:', retryId) | |
| 323 | - // 再次使用nextTick确保DOM更新 | |
| 324 | - await nextTick() | |
| 325 | - } else { | |
| 326 | - console.error('❌ 无法匹配仓库,请检查仓库选项和传入值是否一致') | |
| 327 | - } | |
| 328 | - } | |
| 329 | - } else { | |
| 330 | - console.log('✅ 最终检查:仓库值已正确设置:', finalWarehouse) | |
| 331 | - } | |
| 332 | - } else if (warehouse && warehouseOptions.value.length === 0) { | |
| 333 | - console.warn('⚠️ 仓库选项为空,无法设置仓库值') | |
| 334 | - } | |
| 335 | - | |
| 336 | - searchSerialNumbers().then(() => { | |
| 337 | - nextTick(() => { | |
| 338 | - setSelection() | |
| 339 | - }) | |
| 340 | - }) | |
| 216 | + | |
| 217 | + await searchSerialNumbers() | |
| 218 | + await nextTick() | |
| 219 | + setSelection() | |
| 341 | 220 | } |
| 342 | 221 | |
| 343 | 222 | // 关闭弹窗 |
| ... | ... | @@ -345,6 +224,9 @@ const handleClose = () => { |
| 345 | 224 | visible.value = false |
| 346 | 225 | serialNumberList.value = [] |
| 347 | 226 | selectedSerialNumbers.value = [] |
| 227 | + maxCount.value = 0 | |
| 228 | + currentPage.value = 1 | |
| 229 | + ;(serialTable.value as any)?.clearSelection?.() | |
| 348 | 230 | } |
| 349 | 231 | |
| 350 | 232 | // 查询序列号 |
| ... | ... | @@ -385,6 +267,8 @@ const searchSerialNumbers = async () => { |
| 385 | 267 | serialNumberList.value = [] |
| 386 | 268 | ElMessage.warning('未查询到可用序列号') |
| 387 | 269 | } |
| 270 | + // 重新查询后回到第 1 页 | |
| 271 | + currentPage.value = 1 | |
| 388 | 272 | } catch (error: any) { |
| 389 | 273 | console.error('查询序列号失败:', error) |
| 390 | 274 | if (error && error.message !== '操作成功') { |
| ... | ... | @@ -406,9 +290,25 @@ const resetSearch = () => { |
| 406 | 290 | searchSerialNumbers() |
| 407 | 291 | } |
| 408 | 292 | |
| 409 | -// 处理选择变化 | |
| 293 | +// 处理选择变化(配合 reserve-selection,selection 是跨页的全量已选) | |
| 410 | 294 | const handleSelectionChange = (selection: any[]) => { |
| 411 | - selectedSerialNumbers.value = selection.map(item => item.serialNumber) | |
| 295 | + let next = selection.map(item => String(item.serialNumber || '').trim()).filter(Boolean) | |
| 296 | + // 双保险:即便因极端时序出现超选,截断到 maxCount 并提示 | |
| 297 | + if (maxCount.value > 0 && next.length > maxCount.value) { | |
| 298 | + ElMessage.warning(`最多只能选择 ${maxCount.value} 个序列号`) | |
| 299 | + const keep = new Set(next.slice(0, maxCount.value)) | |
| 300 | + next = next.slice(0, maxCount.value) | |
| 301 | + // 取消多余勾选 | |
| 302 | + nextTick(() => { | |
| 303 | + serialNumberList.value.forEach((row: any) => { | |
| 304 | + const sn = String(row.serialNumber || '').trim() | |
| 305 | + if (!keep.has(sn)) { | |
| 306 | + (serialTable.value as any)?.toggleRowSelection?.(row, false) | |
| 307 | + } | |
| 308 | + }) | |
| 309 | + }) | |
| 310 | + } | |
| 311 | + selectedSerialNumbers.value = next | |
| 412 | 312 | } |
| 413 | 313 | |
| 414 | 314 | // 移除已选择的序列号 |
| ... | ... | @@ -429,6 +329,10 @@ const serialTable = ref() |
| 429 | 329 | // 确认选择 |
| 430 | 330 | const emit = defineEmits(['confirm']) |
| 431 | 331 | const confirmSelection = () => { |
| 332 | + if (maxCount.value > 0 && selectedSerialNumbers.value.length !== maxCount.value) { | |
| 333 | + ElMessage.warning(`商品清单数量为 ${maxCount.value},当前已选 ${selectedSerialNumbers.value.length} 个,数量不一致`) | |
| 334 | + return | |
| 335 | + } | |
| 432 | 336 | emit('confirm', selectedSerialNumbers.value) |
| 433 | 337 | handleClose() |
| 434 | 338 | } |
| ... | ... | @@ -528,6 +432,28 @@ defineExpose({ |
| 528 | 432 | flex-wrap: wrap; |
| 529 | 433 | gap: 5px; |
| 530 | 434 | } |
| 435 | + | |
| 436 | +.count-tip { | |
| 437 | + margin-bottom: 10px; | |
| 438 | + font-size: 13px; | |
| 439 | + color: #606266; | |
| 440 | +} | |
| 441 | + | |
| 442 | +.count-tip .hl { | |
| 443 | + font-weight: 600; | |
| 444 | + color: #409eff; | |
| 445 | + margin: 0 3px; | |
| 446 | +} | |
| 447 | + | |
| 448 | +.count-tip .hl.over { | |
| 449 | + color: #f56c6c; | |
| 450 | +} | |
| 451 | + | |
| 452 | +.pagination-wrap { | |
| 453 | + display: flex; | |
| 454 | + justify-content: flex-end; | |
| 455 | + margin-top: 12px; | |
| 456 | +} | |
| 531 | 457 | </style> |
| 532 | 458 | |
| 533 | 459 | <style> | ... | ... |
Antis.Erp.Plat/douyin/frontend/src/views/CreateWaybillView.vue
| ... | ... | @@ -31,10 +31,40 @@ |
| 31 | 31 | <el-row :gutter="16"> |
| 32 | 32 | <!-- 左列:基本信息 --> |
| 33 | 33 | <el-col :span="10"> |
| 34 | + <!-- 已发货/已打印:回显物流公司 + 物流单号,支持复制 --> | |
| 35 | + <el-form-item v-if="waybillTrackingNumber" label="物流单号"> | |
| 36 | + <div class="tracking-wrap"> | |
| 37 | + <el-tag v-if="waybillLogisticsCompany" type="info" size="small" class="tracking-company"> | |
| 38 | + {{ waybillLogisticsCompany }} | |
| 39 | + </el-tag> | |
| 40 | + <span class="tracking-no" :title="waybillTrackingNumber">{{ waybillTrackingNumber }}</span> | |
| 41 | + <el-button link type="primary" size="small" @click="copySingleOrderId(waybillTrackingNumber)" class="copy-btn">复制</el-button> | |
| 42 | + <el-tag | |
| 43 | + v-if="waybillStatus === 2" | |
| 44 | + type="success" | |
| 45 | + size="small" | |
| 46 | + class="tracking-shipped" | |
| 47 | + >已发货</el-tag> | |
| 48 | + <span v-if="waybillShipTime" class="tracking-time">{{ formatShipTime(waybillShipTime) }}</span> | |
| 49 | + </div> | |
| 50 | + </el-form-item> | |
| 34 | 51 | <el-form-item label="订单号"> |
| 35 | - <div class="order-id-copy"> | |
| 36 | - <span class="order-id-text" :title="form.orderId_Douyin">{{ form.orderId_Douyin }}</span> | |
| 37 | - <el-button link type="primary" size="small" @click="copyOrderId" class="copy-btn">复制</el-button> | |
| 52 | + <div class="order-id-wrap"> | |
| 53 | + <!-- 合并单:逐行显示每个原始订单号 + 独立复制;单订单保持一行 --> | |
| 54 | + <div | |
| 55 | + v-for="(oid, idx) in orderIdList" | |
| 56 | + :key="oid + '_' + idx" | |
| 57 | + class="order-id-row" | |
| 58 | + > | |
| 59 | + <span class="order-id-index" v-if="orderIdList.length > 1">{{ idx + 1 }}</span> | |
| 60 | + <span class="order-id-text" :title="oid">{{ oid }}</span> | |
| 61 | + <el-button link type="primary" size="small" @click="copySingleOrderId(oid)" class="copy-btn">复制</el-button> | |
| 62 | + </div> | |
| 63 | + <!-- 合并单额外给一个「全部复制」 --> | |
| 64 | + <div v-if="orderIdList.length > 1" class="order-id-all"> | |
| 65 | + <el-button link type="success" size="small" @click="copyAllOrderIds">全部复制({{ orderIdList.length }})</el-button> | |
| 66 | + </div> | |
| 67 | + <span v-if="orderIdList.length === 0" style="color:#c0c4cc;">无</span> | |
| 38 | 68 | </div> |
| 39 | 69 | </el-form-item> |
| 40 | 70 | <el-form-item label="订单状态"> |
| ... | ... | @@ -74,21 +104,41 @@ |
| 74 | 104 | </el-alert> |
| 75 | 105 | </el-form-item> |
| 76 | 106 | <el-row :gutter="8"> |
| 77 | - <el-col :span="8"> | |
| 78 | - <el-form-item label="订单金额" label-width="70px"> | |
| 107 | + <el-col :span="12"> | |
| 108 | + <el-form-item label="订单金额" label-width="90px"> | |
| 79 | 109 | <span class="amount-text">{{ formatOrderMoney(form.orderAmountFen) }}</span> |
| 80 | 110 | </el-form-item> |
| 81 | 111 | </el-col> |
| 82 | - <el-col :span="8"> | |
| 83 | - <el-form-item label="实付金额" label-width="70px"> | |
| 112 | + <el-col :span="12"> | |
| 113 | + <el-form-item label="用户支付金额" label-width="90px"> | |
| 84 | 114 | <span class="amount-text amount-pay">{{ formatOrderMoney(form.payAmountFen) }}</span> |
| 85 | 115 | </el-form-item> |
| 86 | 116 | </el-col> |
| 87 | - <el-col :span="8"> | |
| 88 | - <el-form-item label="优惠金额" label-width="70px"> | |
| 117 | + <el-col :span="12"> | |
| 118 | + <el-form-item label="优惠金额" label-width="90px"> | |
| 89 | 119 | <span class="amount-text amount-discount">{{ formatDiscountFen(form.orderAmountFen, form.payAmountFen) }}</span> |
| 90 | 120 | </el-form-item> |
| 91 | 121 | </el-col> |
| 122 | + <el-col :span="12"> | |
| 123 | + <el-form-item label="商家实际收入" label-width="90px"> | |
| 124 | + <el-input-number | |
| 125 | + v-model="form.merchantIncome" | |
| 126 | + :min="0" | |
| 127 | + :precision="2" | |
| 128 | + :step="1" | |
| 129 | + :controls="false" | |
| 130 | + size="small" | |
| 131 | + style="width: 140px;" | |
| 132 | + :disabled="(waybillStatus !== undefined && waybillStatus >= 1) || form.status === 2 || form.status === 3 || form.status === 4" | |
| 133 | + /> | |
| 134 | + <el-tooltip | |
| 135 | + content="默认等于用户支付金额,可修改;提交后写入 ERP 销售出库单的收款金额" | |
| 136 | + placement="top" | |
| 137 | + > | |
| 138 | + <el-icon style="margin-left: 4px; color: #909399; vertical-align: middle;"><InfoFilled /></el-icon> | |
| 139 | + </el-tooltip> | |
| 140 | + </el-form-item> | |
| 141 | + </el-col> | |
| 92 | 142 | </el-row> |
| 93 | 143 | </el-col> |
| 94 | 144 | <!-- 右列:商品明细 --> |
| ... | ... | @@ -100,15 +150,16 @@ |
| 100 | 150 | :key="index" |
| 101 | 151 | class="order-product-item" |
| 102 | 152 | > |
| 153 | + <!-- 商品明细:显示抖音图,没有则显示"暂无图片" --> | |
| 103 | 154 | <el-image |
| 104 | - v-if="item.product_pic" | |
| 105 | - :src="item.product_pic" | |
| 155 | + v-if="item.douyin_pic || item.product_pic" | |
| 156 | + :src="item.douyin_pic || item.product_pic" | |
| 106 | 157 | class="product-thumb" |
| 107 | 158 | fit="cover" |
| 108 | - :preview-src-list="[item.product_pic]" | |
| 159 | + :preview-src-list="[item.douyin_pic || item.product_pic]" | |
| 109 | 160 | preview-teleported |
| 110 | 161 | /> |
| 111 | - <div class="product-thumb-placeholder" v-else /> | |
| 162 | + <div class="product-thumb-placeholder" v-else>暂无图片</div> | |
| 112 | 163 | <div class="product-info"> |
| 113 | 164 | <div class="product-name">{{ item.product_name }}</div> |
| 114 | 165 | <div v-if="getSkuCode(item)" class="product-sku"> |
| ... | ... | @@ -167,6 +218,16 @@ |
| 167 | 218 | <div class="section-header"> |
| 168 | 219 | <span class="section-title">商品清单</span> |
| 169 | 220 | <div class="product-header-selects"> |
| 221 | + <el-form-item label="发货人" required class="inline-select"> | |
| 222 | + <el-select v-model="form.fhr" placeholder="请选择" clearable filterable size="small"> | |
| 223 | + <el-option | |
| 224 | + v-for="u in userOptions" | |
| 225 | + :key="u.id" | |
| 226 | + :label="u.displayName || u.realName || u.account || u.id" | |
| 227 | + :value="u.id" | |
| 228 | + /> | |
| 229 | + </el-select> | |
| 230 | + </el-form-item> | |
| 170 | 231 | <el-form-item label="出库仓库" required class="inline-select"> |
| 171 | 232 | <el-select v-model="form.cjck" placeholder="请选择" clearable size="small"> |
| 172 | 233 | <el-option v-for="item in warehouseOptions" :key="item.id || item.F_Id" :label="item.mdmc || item.F_mdmc || item.Mdmc" :value="item.id || item.F_Id" /> |
| ... | ... | @@ -179,7 +240,7 @@ |
| 179 | 240 | </el-form-item> |
| 180 | 241 | <el-form-item label="收款账户" required class="inline-select"> |
| 181 | 242 | <el-select v-model="form.skzh" placeholder="请选择" clearable filterable size="small" :disabled="shopSettingsLocked.skzh"> |
| 182 | - <el-option v-for="item in paymentAccountOptions" :key="item.skzhDictId || item.id || item.F_Id" :label="item.zhmc || item.F_zhmc || item.Zhmc" :value="item.skzhDictId || item.id || item.F_Id" /> | |
| 243 | + <el-option v-for="item in paymentAccountOptions" :key="item.id" :label="item.accountName || item.zhmc || item.id" :value="item.id" /> | |
| 183 | 244 | </el-select> |
| 184 | 245 | </el-form-item> |
| 185 | 246 | <el-button type="primary" size="small" @click="showProductDialog = true">添加ERP商品</el-button> |
| ... | ... | @@ -199,16 +260,17 @@ |
| 199 | 260 | > |
| 200 | 261 | <el-table-column label="商品图片" width="80" align="center"> |
| 201 | 262 | <template #default="{ row }"> |
| 263 | + <!-- 商品清单:显示 ERP 图,没有则显示"暂无图片" --> | |
| 202 | 264 | <el-image |
| 203 | - v-if="row.product_pic" | |
| 204 | - :src="row.product_pic" | |
| 265 | + v-if="row.erp_pic" | |
| 266 | + :src="row.erp_pic" | |
| 205 | 267 | style="width: 50px; height: 50px; border-radius: 4px;" |
| 206 | 268 | fit="cover" |
| 207 | - :preview-src-list="[row.product_pic]" | |
| 269 | + :preview-src-list="[row.erp_pic]" | |
| 208 | 270 | preview-teleported |
| 209 | 271 | lazy |
| 210 | 272 | /> |
| 211 | - <span v-else style="color: #c0c4cc; font-size: 12px;">无图片</span> | |
| 273 | + <div v-else class="cell-no-image">暂无图片</div> | |
| 212 | 274 | </template> |
| 213 | 275 | </el-table-column> |
| 214 | 276 | <el-table-column prop="product_name" label="商品名称" min-width="180" show-overflow-tooltip /> |
| ... | ... | @@ -281,31 +343,59 @@ |
| 281 | 343 | </div> |
| 282 | 344 | </template> |
| 283 | 345 | </el-table-column> |
| 284 | - <el-table-column label="选择序列号" width="150" align="center"> | |
| 346 | + <el-table-column label="选择序列号" width="170" align="center"> | |
| 285 | 347 | <template #default="{ row }"> |
| 286 | 348 | <div class="serial-number-selector"> |
| 287 | - <div v-if="row.selectedSerialNumbers && row.selectedSerialNumbers.length > 0" class="selected-serial-numbers"> | |
| 288 | - <el-tag | |
| 289 | - v-for="(serialNumber, index) in row.selectedSerialNumbers.slice(0, 2)" | |
| 290 | - :key="index" | |
| 291 | - size="small" | |
| 292 | - closable | |
| 293 | - @close="removeSerialNumber(row, serialNumber)" | |
| 294 | - style="margin: 2px;" | |
| 295 | - > | |
| 296 | - {{ serialNumber }} | |
| 297 | - </el-tag> | |
| 298 | - <el-tag v-if="row.selectedSerialNumbers.length > 2" size="small" type="info"> | |
| 299 | - +{{ row.selectedSerialNumbers.length - 2 }} | |
| 300 | - </el-tag> | |
| 301 | - </div> | |
| 302 | - <el-button | |
| 303 | - type="primary" | |
| 304 | - size="small" | |
| 349 | + <!-- 已选状态徽标:悬停或点击查看完整列表,不在清单行内铺 tag --> | |
| 350 | + <el-popover | |
| 351 | + v-if="row.selectedSerialNumbers && row.selectedSerialNumbers.length > 0" | |
| 352 | + placement="top" | |
| 353 | + :width="320" | |
| 354 | + trigger="click" | |
| 355 | + popper-class="sn-popover" | |
| 356 | + > | |
| 357 | + <template #reference> | |
| 358 | + <el-tag | |
| 359 | + :type="getSerialCountTagType(row)" | |
| 360 | + size="small" | |
| 361 | + effect="light" | |
| 362 | + class="sn-count-tag" | |
| 363 | + > | |
| 364 | + 已选 {{ row.selectedSerialNumbers.length }} | |
| 365 | + <span v-if="Number(row.item_num) > 0"> / {{ row.item_num }}</span> | |
| 366 | + </el-tag> | |
| 367 | + </template> | |
| 368 | + | |
| 369 | + <div class="sn-popover-header"> | |
| 370 | + <span>已选序列号 ({{ row.selectedSerialNumbers.length }})</span> | |
| 371 | + <el-button | |
| 372 | + type="danger" | |
| 373 | + link | |
| 374 | + size="small" | |
| 375 | + @click="clearSerialNumbers(row)" | |
| 376 | + >清空</el-button> | |
| 377 | + </div> | |
| 378 | + <div class="sn-popover-list"> | |
| 379 | + <el-tag | |
| 380 | + v-for="(sn, idx) in row.selectedSerialNumbers" | |
| 381 | + :key="idx" | |
| 382 | + size="small" | |
| 383 | + closable | |
| 384 | + @close="removeSerialNumber(row, sn)" | |
| 385 | + class="sn-tag" | |
| 386 | + > | |
| 387 | + {{ sn }} | |
| 388 | + </el-tag> | |
| 389 | + </div> | |
| 390 | + </el-popover> | |
| 391 | + | |
| 392 | + <el-button | |
| 393 | + type="primary" | |
| 394 | + size="small" | |
| 305 | 395 | @click="openSerialNumberSelect(row)" |
| 306 | 396 | :disabled="!row.spbm && !getSkuCode(row)" |
| 307 | 397 | > |
| 308 | - 选择序列号 | |
| 398 | + {{ row.selectedSerialNumbers && row.selectedSerialNumbers.length > 0 ? '修改序列号' : '选择序列号' }} | |
| 309 | 399 | </el-button> |
| 310 | 400 | </div> |
| 311 | 401 | </template> |
| ... | ... | @@ -435,8 +525,8 @@ |
| 435 | 525 | import { ref, computed, onMounted, nextTick } from 'vue' |
| 436 | 526 | import { useRoute, useRouter } from 'vue-router' |
| 437 | 527 | import { ElMessage, ElMessageBox } from 'element-plus' |
| 438 | -import { Refresh } from '@element-plus/icons-vue' | |
| 439 | -import { getOrderDetail, getMergedOrderDetail, searchProducts as searchProductsAPI, manualCreateWaybill, getProductInfo as getProductInfoAPI, getWarehouses, getProductCategories, updateSellerRemark, getDefaults, getCustomers, getPaymentAccounts, getShopSettings, checkStock } from '@/api/order' | |
| 528 | +import { Refresh, InfoFilled } from '@element-plus/icons-vue' | |
| 529 | +import { getOrderDetail, getMergedOrderDetail, searchProducts as searchProductsAPI, manualCreateWaybill, getProductInfo as getProductInfoAPI, getWarehouses, getProductCategories, updateSellerRemark, getDefaults, getCustomers, getPaymentAccounts, getUsers, getShopSettings, checkStock } from '@/api/order' | |
| 440 | 530 | import { getErpBaseUrl } from '@/utils/config' |
| 441 | 531 | import SerialNumberSelect from '@/components/SerialNumberSelect.vue' |
| 442 | 532 | |
| ... | ... | @@ -450,6 +540,8 @@ const orderId = ref<number>(Number(route.params.id || 0)) |
| 450 | 540 | const mergedInternalIds = ref<number[]>([]) |
| 451 | 541 | const tableKey = ref(0) // 用于强制更新表格 |
| 452 | 542 | const serialNumberSelectRef = ref() |
| 543 | +// 当前打开序列号弹窗对应的商品行引用(选完序列号后直接写回这一行,避免 spbm 被弹窗改动导致匹配错位) | |
| 544 | +const currentSerialRow = ref<any | null>(null) | |
| 453 | 545 | |
| 454 | 546 | // 商品信息缓存(用于存储序列号类型) |
| 455 | 547 | const productCache = new Map<string, any>() |
| ... | ... | @@ -481,20 +573,40 @@ const formatDiscountFen = (orderFen: number | null | undefined, payFen: number | |
| 481 | 573 | return `¥${(d / 100).toFixed(2)}` |
| 482 | 574 | } |
| 483 | 575 | |
| 484 | -const copyOrderId = () => { | |
| 485 | - const id = form.value.orderId_Douyin | |
| 486 | - if (!id) return | |
| 487 | - navigator.clipboard.writeText(id).then(() => { | |
| 488 | - ElMessage.success('订单号已复制') | |
| 489 | - }).catch(() => { | |
| 576 | +// 订单号列表:合并单时后端以逗号拼接(OrdersController.GetMergedOrderDetail 里 string.Join(",", ...)) | |
| 577 | +const orderIdList = computed<string[]>(() => { | |
| 578 | + const raw = String(form.value.orderId_Douyin || '').trim() | |
| 579 | + if (!raw) return [] | |
| 580 | + return raw.split(/[,\s,;;|]+/).map(s => s.trim()).filter(Boolean) | |
| 581 | +}) | |
| 582 | + | |
| 583 | +const copyTextToClipboard = async (text: string) => { | |
| 584 | + if (!text) return | |
| 585 | + try { | |
| 586 | + await navigator.clipboard.writeText(text) | |
| 587 | + } catch { | |
| 490 | 588 | const ta = document.createElement('textarea') |
| 491 | - ta.value = id | |
| 589 | + ta.value = text | |
| 492 | 590 | document.body.appendChild(ta) |
| 493 | 591 | ta.select() |
| 494 | 592 | document.execCommand('copy') |
| 495 | 593 | document.body.removeChild(ta) |
| 496 | - ElMessage.success('订单号已复制') | |
| 497 | - }) | |
| 594 | + } | |
| 595 | +} | |
| 596 | + | |
| 597 | +// 单个订单号复制 | |
| 598 | +const copySingleOrderId = async (id: string) => { | |
| 599 | + if (!id) return | |
| 600 | + await copyTextToClipboard(id) | |
| 601 | + ElMessage.success(`订单号 ${id} 已复制`) | |
| 602 | +} | |
| 603 | + | |
| 604 | +// 全部订单号复制(逗号分隔,方便粘贴到 Excel) | |
| 605 | +const copyAllOrderIds = async () => { | |
| 606 | + const all = orderIdList.value.join(',') | |
| 607 | + if (!all) return | |
| 608 | + await copyTextToClipboard(all) | |
| 609 | + ElMessage.success(`已复制 ${orderIdList.value.length} 个订单号`) | |
| 498 | 610 | } |
| 499 | 611 | |
| 500 | 612 | const form = ref({ |
| ... | ... | @@ -522,7 +634,11 @@ const form = ref({ |
| 522 | 634 | kh: '', |
| 523 | 635 | skzh: '', |
| 524 | 636 | productItems: [] as any[], |
| 525 | - remark: '' | |
| 637 | + remark: '', | |
| 638 | + /** 商家实际收入(元)。默认 = 用户支付金额(元),用户可改;最终写入 ERP 销售出库单的收款金额 */ | |
| 639 | + merchantIncome: null as number | null, | |
| 640 | + /** 发货人(ERP 用户 Id),必填;写入 ERP 销售出库单 fhr */ | |
| 641 | + fhr: '' | |
| 526 | 642 | }) |
| 527 | 643 | |
| 528 | 644 | // 订单备注 |
| ... | ... | @@ -534,12 +650,27 @@ const remarkSyncing = ref(false) |
| 534 | 650 | const warehouseOptions = ref<any[]>([]) |
| 535 | 651 | const customerOptions = ref<any[]>([]) |
| 536 | 652 | const paymentAccountOptions = ref<any[]>([]) |
| 653 | +// 发货人(ERP 用户)选项 | |
| 654 | +const userOptions = ref<{ id: string; realName?: string; account?: string; displayName?: string }[]>([]) | |
| 537 | 655 | |
| 538 | 656 | /** 往来单位/收款账户是否由店铺设置锁定(不可改) */ |
| 539 | 657 | const shopSettingsLocked = ref({ kh: false, skzh: false }) |
| 540 | 658 | |
| 541 | 659 | // 发货单状态(0-待打印,1-已打印,2-已发货) |
| 542 | 660 | const waybillStatus = ref<number | undefined>(undefined) |
| 661 | +// 已发货时回显的物流信息 | |
| 662 | +const waybillTrackingNumber = ref('') | |
| 663 | +const waybillLogisticsCompany = ref('') | |
| 664 | +const waybillShipTime = ref<string>('') | |
| 665 | + | |
| 666 | +// 格式化发货时间 | |
| 667 | +const formatShipTime = (val: string): string => { | |
| 668 | + if (!val) return '' | |
| 669 | + const d = new Date(val) | |
| 670 | + if (isNaN(d.getTime())) return String(val) | |
| 671 | + const pad = (n: number) => n.toString().padStart(2, '0') | |
| 672 | + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}` | |
| 673 | +} | |
| 543 | 674 | |
| 544 | 675 | // 商品选择对话框 |
| 545 | 676 | const showProductDialog = ref(false) |
| ... | ... | @@ -594,6 +725,18 @@ const loadPaymentAccounts = async () => { |
| 594 | 725 | } |
| 595 | 726 | } |
| 596 | 727 | |
| 728 | +// 加载 ERP 用户列表(发货人下拉) | |
| 729 | +const loadUsers = async () => { | |
| 730 | + try { | |
| 731 | + const response = await getUsers() | |
| 732 | + if (response.data && response.data.code === 200) { | |
| 733 | + userOptions.value = response.data.data || [] | |
| 734 | + } | |
| 735 | + } catch (error: any) { | |
| 736 | + console.error('加载用户列表失败:', error) | |
| 737 | + } | |
| 738 | +} | |
| 739 | + | |
| 597 | 740 | // 加载订单详情(支持合并订单) |
| 598 | 741 | const loadOrderDetail = async () => { |
| 599 | 742 | try { |
| ... | ... | @@ -648,13 +791,39 @@ const loadOrderDetail = async () => { |
| 648 | 791 | if (data.waybill.status !== undefined) { |
| 649 | 792 | waybillStatus.value = data.waybill.status |
| 650 | 793 | } |
| 794 | + // 已发货单的物流信息回显 | |
| 795 | + waybillTrackingNumber.value = data.waybill.trackingNumber || '' | |
| 796 | + waybillLogisticsCompany.value = data.waybill.logisticsCompany || '' | |
| 797 | + waybillShipTime.value = data.waybill.shipTime || '' | |
| 798 | + if (data.waybill.logisticsCompany) { | |
| 799 | + form.value.logisticsCompany = data.waybill.logisticsCompany | |
| 800 | + } | |
| 801 | + // 发货人:有历史值就回显,没有等待 loadUsers 完成后也不主动默认(由用户选择) | |
| 802 | + if (data.waybill.fhr) { | |
| 803 | + form.value.fhr = data.waybill.fhr | |
| 804 | + } | |
| 805 | + // 商家实际收入:有已保存值就回显,否则默认等于用户支付金额(元) | |
| 806 | + if (data.waybill.merchantIncome !== null && data.waybill.merchantIncome !== undefined && data.waybill.merchantIncome !== '') { | |
| 807 | + form.value.merchantIncome = Number(data.waybill.merchantIncome) | |
| 808 | + } else { | |
| 809 | + form.value.merchantIncome = form.value.payAmountFen != null | |
| 810 | + ? Number((form.value.payAmountFen / 100).toFixed(2)) | |
| 811 | + : 0 | |
| 812 | + } | |
| 651 | 813 | } else { |
| 652 | 814 | waybillStatus.value = undefined |
| 815 | + waybillTrackingNumber.value = '' | |
| 816 | + waybillLogisticsCompany.value = '' | |
| 817 | + waybillShipTime.value = '' | |
| 818 | + // 新发货单:默认商家实际收入 = 用户支付金额(元) | |
| 819 | + form.value.merchantIncome = form.value.payAmountFen != null | |
| 820 | + ? Number((form.value.payAmountFen / 100).toFixed(2)) | |
| 821 | + : 0 | |
| 653 | 822 | } |
| 654 | 823 | |
| 655 | 824 | shopSettingsLocked.value = { kh: false, skzh: false } |
| 656 | 825 | |
| 657 | - // 按店铺获取专属 kh/skzh | |
| 826 | + // 按店铺获取专属 cjck/kh/skzh(抖音发货单的出库仓库只来自店铺设置,不再取 ERP 全局默认) | |
| 658 | 827 | const shopId = data.order.shopId |
| 659 | 828 | if (shopId) { |
| 660 | 829 | try { |
| ... | ... | @@ -663,6 +832,10 @@ const loadOrderDetail = async () => { |
| 663 | 832 | console.log('[店铺设置] 响应:', JSON.stringify(shopRes.data)) |
| 664 | 833 | const shopData = shopRes.data?.data |
| 665 | 834 | if (shopData) { |
| 835 | + if (shopData.ck && !form.value.cjck) { | |
| 836 | + form.value.cjck = shopData.ck | |
| 837 | + console.log('[店铺设置] ck =', shopData.ck) | |
| 838 | + } | |
| 666 | 839 | if (shopData.kh) { |
| 667 | 840 | form.value.kh = shopData.kh |
| 668 | 841 | shopSettingsLocked.value.kh = true |
| ... | ... | @@ -677,13 +850,13 @@ const loadOrderDetail = async () => { |
| 677 | 850 | console.warn('[店铺设置] shopData 为空,响应体:', shopRes.data) |
| 678 | 851 | } |
| 679 | 852 | } catch (e) { |
| 680 | - console.warn('[店铺设置] 获取失败,将使用全局默认', e) | |
| 853 | + console.warn('[店铺设置] 获取失败', e) | |
| 681 | 854 | } |
| 682 | 855 | } else { |
| 683 | 856 | console.warn('[店铺设置] 订单无 shopId,跳过店铺设置加载') |
| 684 | 857 | } |
| 685 | 858 | |
| 686 | - // 全局默认设置兜底(出库仓库 / 往来单位 / 收款账户) | |
| 859 | + // 全局默认兜底(出库仓库 / 往来单位 / 收款账户) | |
| 687 | 860 | try { |
| 688 | 861 | const defaultsRes = await getDefaults() |
| 689 | 862 | const defaults = defaultsRes.data |
| ... | ... | @@ -715,10 +888,18 @@ const loadOrderDetail = async () => { |
| 715 | 888 | } |
| 716 | 889 | |
| 717 | 890 | // 尝试多种可能的字段名 |
| 891 | + // 图片拆两个字段: | |
| 892 | + // douyin_pic = 抖音原图(用于"商品明细") | |
| 893 | + // erp_pic = ERP 图(用于"商品清单";由 loadSerialNumberType 匹配 ERP 商品后填充) | |
| 894 | + // 兼容老发货单:若保存的老数据里只有 product_pic,无法再区分,统一作为 douyin_pic 兜底 | |
| 718 | 895 | const rawPic = item.product_pic || item.ProductPic || item.pic || item.image || '' |
| 896 | + const rawDouyinPic = item.douyin_pic || item.DouyinPic || '' | |
| 897 | + const rawErpPic = item.erp_pic || item.ErpPic || '' | |
| 719 | 898 | const mappedItem = { |
| 720 | 899 | product_name: item.product_name || item.ProductName || item.spmc || item.Spmc || item.name || '', |
| 721 | 900 | product_pic: resolveProductPicUrl(rawPic), |
| 901 | + douyin_pic: resolveProductPicUrl(rawDouyinPic || rawPic), | |
| 902 | + erp_pic: resolveProductPicUrl(rawErpPic), | |
| 722 | 903 | item_num: item.item_num || item.ItemNum || item.itemNum || item.quantity || item.num || 1, |
| 723 | 904 | goods_price: goodsPrice, |
| 724 | 905 | spec: item.spec || item.Spec || item.gg || item.Gg || item.specification || '', |
| ... | ... | @@ -821,6 +1002,9 @@ const addProduct = async (product: any) => { |
| 821 | 1002 | const newProduct = { |
| 822 | 1003 | product_name: product.spmc || '', |
| 823 | 1004 | product_pic: resolveProductPicUrl(productPic || product.imageUrl || ''), |
| 1005 | + // ERP 添加的商品没有抖音图,ERP 图直接填 erp_pic,用于"商品清单"展示 | |
| 1006 | + douyin_pic: '', | |
| 1007 | + erp_pic: resolveProductPicUrl(productPic || product.imageUrl || ''), | |
| 824 | 1008 | spbm: product.spbm || '', |
| 825 | 1009 | sku_id: product.dyspid || '', |
| 826 | 1010 | item_num: 1, |
| ... | ... | @@ -917,8 +1101,9 @@ const loadSerialNumberType = async (item: any) => { |
| 917 | 1101 | if (bySku.spxlhType !== undefined && bySku.spxlhType !== null && String(bySku.spxlhType) !== '') { |
| 918 | 1102 | item.spxlhType = String(bySku.spxlhType) |
| 919 | 1103 | } |
| 1104 | + // ERP 图片写到 erp_pic,不覆盖抖音原图 douyin_pic | |
| 920 | 1105 | if (bySku.imageUrl) { |
| 921 | - item.product_pic = resolveProductPicUrl(bySku.imageUrl) | |
| 1106 | + item.erp_pic = resolveProductPicUrl(bySku.imageUrl) | |
| 922 | 1107 | } |
| 923 | 1108 | } |
| 924 | 1109 | } |
| ... | ... | @@ -929,7 +1114,7 @@ const loadSerialNumberType = async (item: any) => { |
| 929 | 1114 | if (response.data?.data?.length > 0) { |
| 930 | 1115 | const product = response.data.data[0] |
| 931 | 1116 | if (product.spbm) item.spbm = product.spbm |
| 932 | - if (product.imageUrl) item.product_pic = resolveProductPicUrl(product.imageUrl) | |
| 1117 | + if (product.imageUrl) item.erp_pic = resolveProductPicUrl(product.imageUrl) | |
| 933 | 1118 | } |
| 934 | 1119 | } catch { /* ignore */ } |
| 935 | 1120 | } |
| ... | ... | @@ -941,7 +1126,7 @@ const loadSerialNumberType = async (item: any) => { |
| 941 | 1126 | item.spxlhType = String(product.spxlhType) |
| 942 | 1127 | } |
| 943 | 1128 | if (product.imageUrl) { |
| 944 | - item.product_pic = resolveProductPicUrl(product.imageUrl) | |
| 1129 | + item.erp_pic = resolveProductPicUrl(product.imageUrl) | |
| 945 | 1130 | } |
| 946 | 1131 | } |
| 947 | 1132 | } |
| ... | ... | @@ -1045,37 +1230,38 @@ const getProductCodeForSerialNumber = async (row: any): Promise<string | null> = |
| 1045 | 1230 | const openSerialNumberSelect = async (row: any) => { |
| 1046 | 1231 | // 获取商品编码(可能通过sku_id查找) |
| 1047 | 1232 | const productCode = await getProductCodeForSerialNumber(row) |
| 1048 | - | |
| 1233 | + | |
| 1049 | 1234 | if (!productCode) { |
| 1050 | 1235 | ElMessage.warning('无法获取商品编码,请确保商品已关联ERP系统') |
| 1051 | 1236 | return |
| 1052 | 1237 | } |
| 1053 | - | |
| 1238 | + | |
| 1239 | + // 记住当前操作的行,选完序列号直接写回这一行 | |
| 1240 | + currentSerialRow.value = row | |
| 1241 | + | |
| 1242 | + // 把商品清单里的需求数量传进去,用于弹窗内限制勾选上限 | |
| 1243 | + const need = Number(row.item_num) || 0 | |
| 1054 | 1244 | serialNumberSelectRef.value?.open( |
| 1055 | 1245 | productCode, |
| 1056 | 1246 | form.value.cjck || '', |
| 1057 | 1247 | row.selectedSerialNumbers || [], |
| 1058 | - '发货单' | |
| 1248 | + '发货单', | |
| 1249 | + need | |
| 1059 | 1250 | ) |
| 1060 | 1251 | } |
| 1061 | 1252 | |
| 1062 | 1253 | // 处理序列号选择 |
| 1063 | 1254 | const handleSerialNumberSelect = (selectedSerialNumbers: string[]) => { |
| 1064 | - const currentProductCode = serialNumberSelectRef.value?.currentProductCode | |
| 1065 | - const currentRow = form.value.productItems.find((item: any) => item.spbm === currentProductCode) | |
| 1066 | - | |
| 1067 | - if (!currentRow && currentProductCode) { | |
| 1068 | - for (const item of form.value.productItems) { | |
| 1069 | - if (!item.spbm && getSkuCode(item)) { | |
| 1070 | - item.spbm = currentProductCode | |
| 1071 | - item.selectedSerialNumbers = selectedSerialNumbers | |
| 1072 | - const count = selectedSerialNumbers.length | |
| 1073 | - if (Number(item.item_num) !== count) { | |
| 1074 | - item.item_num = count | |
| 1075 | - ElMessage.info(`商品"${item.product_name}"的数量已自动更新为 ${count}`) | |
| 1076 | - } | |
| 1077 | - return | |
| 1078 | - } | |
| 1255 | + // 优先使用打开弹窗时保存的行引用,最稳妥 | |
| 1256 | + let currentRow = currentSerialRow.value | |
| 1257 | + | |
| 1258 | + // 兼容老逻辑:如果引用丢失(例如表格重建),再退回 productCode 匹配 | |
| 1259 | + if (!currentRow) { | |
| 1260 | + const currentProductCode = serialNumberSelectRef.value?.currentProductCode | |
| 1261 | + if (currentProductCode) { | |
| 1262 | + currentRow = form.value.productItems.find((item: any) => | |
| 1263 | + item.spbm === currentProductCode || getSkuCode(item) === currentProductCode | |
| 1264 | + ) || null | |
| 1079 | 1265 | } |
| 1080 | 1266 | } |
| 1081 | 1267 | |
| ... | ... | @@ -1086,7 +1272,12 @@ const handleSerialNumberSelect = (selectedSerialNumbers: string[]) => { |
| 1086 | 1272 | currentRow.item_num = count |
| 1087 | 1273 | ElMessage.info(`商品"${currentRow.product_name}"的数量已自动更新为 ${count}`) |
| 1088 | 1274 | } |
| 1275 | + } else { | |
| 1276 | + ElMessage.warning('未能匹配到需要更新的商品行,请重新打开序列号弹窗') | |
| 1089 | 1277 | } |
| 1278 | + | |
| 1279 | + // 关闭后清空,避免串行到下一行 | |
| 1280 | + currentSerialRow.value = null | |
| 1090 | 1281 | } |
| 1091 | 1282 | |
| 1092 | 1283 | // 移除序列号 |
| ... | ... | @@ -1099,6 +1290,22 @@ const removeSerialNumber = (row: any, serialNumber: string) => { |
| 1099 | 1290 | } |
| 1100 | 1291 | } |
| 1101 | 1292 | |
| 1293 | +// 清空某行全部已选序列号 | |
| 1294 | +const clearSerialNumbers = (row: any) => { | |
| 1295 | + if (!row) return | |
| 1296 | + row.selectedSerialNumbers = [] | |
| 1297 | +} | |
| 1298 | + | |
| 1299 | +// 根据"已选数量 vs 需求数量"计算徽标颜色:不够→警告,刚好→成功,超了→红色 | |
| 1300 | +const getSerialCountTagType = (row: any): 'success' | 'warning' | 'danger' | 'info' => { | |
| 1301 | + const need = Number(row?.item_num) || 0 | |
| 1302 | + const got = row?.selectedSerialNumbers?.length || 0 | |
| 1303 | + if (!need) return 'info' | |
| 1304 | + if (got === need) return 'success' | |
| 1305 | + if (got > need) return 'danger' | |
| 1306 | + return 'warning' | |
| 1307 | +} | |
| 1308 | + | |
| 1102 | 1309 | // 获取SKU编码(优先使用sku_id,和订单列表保持一致) |
| 1103 | 1310 | const getSkuCode = (item: any): string => { |
| 1104 | 1311 | if (!item) return '' |
| ... | ... | @@ -1230,6 +1437,7 @@ const handleSubmit = async () => { |
| 1230 | 1437 | return |
| 1231 | 1438 | } |
| 1232 | 1439 | |
| 1440 | + if (!form.value.fhr) { ElMessage.warning('请选择发货人'); return } | |
| 1233 | 1441 | if (!form.value.cjck) { ElMessage.warning('请选择出库仓库'); return } |
| 1234 | 1442 | if (!form.value.kh) { ElMessage.warning('请选择往来单位'); return } |
| 1235 | 1443 | if (!form.value.skzh) { ElMessage.warning('请选择收款账户'); return } |
| ... | ... | @@ -1269,13 +1477,13 @@ const handleSubmit = async () => { |
| 1269 | 1477 | |
| 1270 | 1478 | try { |
| 1271 | 1479 | await ElMessageBox.confirm( |
| 1272 | - '确定提交发货单吗?系统将自动校验库存,库存充足时生成「销售出库单」,库存不足时自动生成「预售出库单」。', | |
| 1480 | + '确定提交发货单吗?系统会校验库存,库存充足时生成「销售出库单」;库存不足将直接拒绝发货,请先到 ERP 补充库存。', | |
| 1273 | 1481 | '提示', |
| 1274 | 1482 | { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' } |
| 1275 | 1483 | ) |
| 1276 | 1484 | } catch { return } |
| 1277 | 1485 | |
| 1278 | - // 库存检查 | |
| 1486 | + // 库存检查:库存不足直接拒绝,不再转预售出库单 | |
| 1279 | 1487 | if (form.value.cjck && form.value.productItems.length > 0) { |
| 1280 | 1488 | try { |
| 1281 | 1489 | const stockItems = form.value.productItems |
| ... | ... | @@ -1286,7 +1494,6 @@ const handleSubmit = async () => { |
| 1286 | 1494 | const stockRes = await checkStock(form.value.cjck, stockItems) |
| 1287 | 1495 | const stockData = stockRes.data |
| 1288 | 1496 | |
| 1289 | - // 检查是否有库存不足的商品 | |
| 1290 | 1497 | const insufficientItems = stockData?.data?.filter?.((item: any) => !item.sufficient) || [] |
| 1291 | 1498 | |
| 1292 | 1499 | if (insufficientItems.length > 0) { |
| ... | ... | @@ -1294,21 +1501,12 @@ const handleSubmit = async () => { |
| 1294 | 1501 | `${item.spmc || item.spbm}(库存: ${item.stock ?? 0},需要: ${item.quantity})` |
| 1295 | 1502 | ).join('\n') |
| 1296 | 1503 | |
| 1297 | - try { | |
| 1298 | - await ElMessageBox.confirm( | |
| 1299 | - `以下商品库存不足:\n\n${itemNames}\n\n选择「继续发货」将自动生成预售出库单。`, | |
| 1300 | - '库存不足', | |
| 1301 | - { | |
| 1302 | - confirmButtonText: '继续发货(转预售出库单)', | |
| 1303 | - cancelButtonText: '取消发货', | |
| 1304 | - type: 'warning', | |
| 1305 | - distinguishCancelAndClose: true | |
| 1306 | - } | |
| 1307 | - ) | |
| 1308 | - // 用户选择继续 → 不做额外处理,后端 CreateSalesOrderAsync 的 BatchCheckStock 会自动转预售 | |
| 1309 | - } catch { | |
| 1310 | - return // 用户取消 | |
| 1311 | - } | |
| 1504 | + await ElMessageBox.alert( | |
| 1505 | + `以下商品库存不足,无法发货:\n\n${itemNames}\n\n请先到 ERP 补充库存后再提交发货单。`, | |
| 1506 | + '库存不足', | |
| 1507 | + { confirmButtonText: '我知道了', type: 'error' } | |
| 1508 | + ) | |
| 1509 | + return | |
| 1312 | 1510 | } |
| 1313 | 1511 | } |
| 1314 | 1512 | } catch (e) { |
| ... | ... | @@ -1320,7 +1518,10 @@ const handleSubmit = async () => { |
| 1320 | 1518 | try { |
| 1321 | 1519 | const productItemsForSubmit = form.value.productItems.map((item: any) => ({ |
| 1322 | 1520 | product_name: item.product_name || '', |
| 1323 | - product_pic: item.product_pic || '', | |
| 1521 | + // product_pic 保留以兼容老后端;同时新增 douyin_pic/erp_pic 分别存抖音图和 ERP 图 | |
| 1522 | + product_pic: item.erp_pic || item.product_pic || item.douyin_pic || '', | |
| 1523 | + douyin_pic: item.douyin_pic || '', | |
| 1524 | + erp_pic: item.erp_pic || '', | |
| 1324 | 1525 | item_num: Number(item.item_num) || 1, |
| 1325 | 1526 | goods_price: Math.round((Number(item.goods_price) || 0) * 100), |
| 1326 | 1527 | spec: item.spec || '', |
| ... | ... | @@ -1352,7 +1553,9 @@ const handleSubmit = async () => { |
| 1352 | 1553 | productItems: productItemsForSubmit, |
| 1353 | 1554 | remark: form.value.remark, |
| 1354 | 1555 | douyinOrderIds: form.value.orderId_Douyin || '', |
| 1355 | - mergedOrderInternalIds: mergedInternalIds.value.length > 1 ? mergedInternalIds.value : undefined | |
| 1556 | + mergedOrderInternalIds: mergedInternalIds.value.length > 1 ? mergedInternalIds.value : undefined, | |
| 1557 | + merchantIncome: form.value.merchantIncome, | |
| 1558 | + fhr: form.value.fhr | |
| 1356 | 1559 | }) |
| 1357 | 1560 | |
| 1358 | 1561 | ElMessage.success(response.data.message || '发货单创建成功') |
| ... | ... | @@ -1376,6 +1579,7 @@ onMounted(() => { |
| 1376 | 1579 | loadWarehouses() |
| 1377 | 1580 | loadCustomers() |
| 1378 | 1581 | loadPaymentAccounts() |
| 1582 | + loadUsers() | |
| 1379 | 1583 | loadOrderDetail() |
| 1380 | 1584 | }) |
| 1381 | 1585 | </script> |
| ... | ... | @@ -1492,11 +1696,33 @@ onMounted(() => { |
| 1492 | 1696 | } |
| 1493 | 1697 | |
| 1494 | 1698 | /* ── 订单号复制 ── */ |
| 1495 | -.order-id-copy { | |
| 1699 | +.order-id-wrap { | |
| 1700 | + display: flex; | |
| 1701 | + flex-direction: column; | |
| 1702 | + gap: 2px; | |
| 1703 | + min-height: 28px; | |
| 1704 | +} | |
| 1705 | + | |
| 1706 | +.order-id-row { | |
| 1496 | 1707 | display: flex; |
| 1497 | 1708 | align-items: center; |
| 1498 | 1709 | gap: 6px; |
| 1499 | - min-height: 28px; | |
| 1710 | + line-height: 22px; | |
| 1711 | +} | |
| 1712 | + | |
| 1713 | +.order-id-index { | |
| 1714 | + display: inline-flex; | |
| 1715 | + align-items: center; | |
| 1716 | + justify-content: center; | |
| 1717 | + min-width: 18px; | |
| 1718 | + height: 18px; | |
| 1719 | + padding: 0 4px; | |
| 1720 | + background: #ecf5ff; | |
| 1721 | + color: #409eff; | |
| 1722 | + border-radius: 9px; | |
| 1723 | + font-size: 11px; | |
| 1724 | + font-weight: 600; | |
| 1725 | + flex-shrink: 0; | |
| 1500 | 1726 | } |
| 1501 | 1727 | |
| 1502 | 1728 | .order-id-text { |
| ... | ... | @@ -1507,12 +1733,47 @@ onMounted(() => { |
| 1507 | 1733 | word-break: break-all; |
| 1508 | 1734 | } |
| 1509 | 1735 | |
| 1736 | +.order-id-all { | |
| 1737 | + margin-top: 2px; | |
| 1738 | +} | |
| 1739 | + | |
| 1510 | 1740 | .copy-btn { |
| 1511 | 1741 | flex-shrink: 0; |
| 1512 | 1742 | font-size: 12px; |
| 1513 | 1743 | padding: 0 4px; |
| 1514 | 1744 | } |
| 1515 | 1745 | |
| 1746 | +/* ── 物流单号回显 ── */ | |
| 1747 | +.tracking-wrap { | |
| 1748 | + display: flex; | |
| 1749 | + align-items: center; | |
| 1750 | + flex-wrap: wrap; | |
| 1751 | + gap: 6px; | |
| 1752 | + min-height: 28px; | |
| 1753 | +} | |
| 1754 | + | |
| 1755 | +.tracking-company { | |
| 1756 | + flex-shrink: 0; | |
| 1757 | +} | |
| 1758 | + | |
| 1759 | +.tracking-no { | |
| 1760 | + font-size: 13px; | |
| 1761 | + font-weight: 600; | |
| 1762 | + color: #303133; | |
| 1763 | + letter-spacing: 0.2px; | |
| 1764 | + user-select: text; | |
| 1765 | + word-break: break-all; | |
| 1766 | +} | |
| 1767 | + | |
| 1768 | +.tracking-shipped { | |
| 1769 | + flex-shrink: 0; | |
| 1770 | +} | |
| 1771 | + | |
| 1772 | +.tracking-time { | |
| 1773 | + font-size: 12px; | |
| 1774 | + color: #909399; | |
| 1775 | +} | |
| 1776 | + | |
| 1516 | 1777 | /* ── 金额 ── */ |
| 1517 | 1778 | .amount-text { |
| 1518 | 1779 | font-size: 13px; |
| ... | ... | @@ -1574,6 +1835,30 @@ onMounted(() => { |
| 1574 | 1835 | flex-shrink: 0; |
| 1575 | 1836 | background: #f5f7fa; |
| 1576 | 1837 | border: 1px dashed #dcdfe6; |
| 1838 | + display: flex; | |
| 1839 | + align-items: center; | |
| 1840 | + justify-content: center; | |
| 1841 | + color: #c0c4cc; | |
| 1842 | + font-size: 11px; | |
| 1843 | + text-align: center; | |
| 1844 | + line-height: 1.2; | |
| 1845 | +} | |
| 1846 | + | |
| 1847 | +/* 商品清单单元格内的"暂无图片"占位 */ | |
| 1848 | +.cell-no-image { | |
| 1849 | + width: 50px; | |
| 1850 | + height: 50px; | |
| 1851 | + margin: 0 auto; | |
| 1852 | + border-radius: 4px; | |
| 1853 | + background: #f5f7fa; | |
| 1854 | + border: 1px dashed #dcdfe6; | |
| 1855 | + display: flex; | |
| 1856 | + align-items: center; | |
| 1857 | + justify-content: center; | |
| 1858 | + color: #c0c4cc; | |
| 1859 | + font-size: 11px; | |
| 1860 | + text-align: center; | |
| 1861 | + line-height: 1.2; | |
| 1577 | 1862 | } |
| 1578 | 1863 | |
| 1579 | 1864 | .product-info { |
| ... | ... | @@ -1670,16 +1955,19 @@ onMounted(() => { |
| 1670 | 1955 | |
| 1671 | 1956 | /* ── 序列号 ── */ |
| 1672 | 1957 | .serial-number-selector { |
| 1958 | + display: flex; | |
| 1959 | + flex-direction: column; | |
| 1960 | + align-items: center; | |
| 1961 | + gap: 4px; | |
| 1673 | 1962 | min-height: 30px; |
| 1674 | 1963 | } |
| 1675 | 1964 | |
| 1676 | -.selected-serial-numbers { | |
| 1677 | - display: flex; | |
| 1678 | - flex-wrap: wrap; | |
| 1679 | - gap: 2px; | |
| 1680 | - margin-bottom: 4px; | |
| 1965 | +.sn-count-tag { | |
| 1966 | + cursor: pointer; | |
| 1967 | + user-select: none; | |
| 1681 | 1968 | } |
| 1682 | 1969 | |
| 1970 | + | |
| 1683 | 1971 | /* ── 滚动条 ── */ |
| 1684 | 1972 | .order-product-list::-webkit-scrollbar { |
| 1685 | 1973 | width: 5px; |
| ... | ... | @@ -1705,3 +1993,29 @@ onMounted(() => { |
| 1705 | 1993 | } |
| 1706 | 1994 | </style> |
| 1707 | 1995 | |
| 1996 | +<!-- 非 scoped:el-popover teleport 到 body,scoped 选择器命中不到 --> | |
| 1997 | +<style> | |
| 1998 | +.sn-popover .sn-popover-header { | |
| 1999 | + display: flex; | |
| 2000 | + justify-content: space-between; | |
| 2001 | + align-items: center; | |
| 2002 | + margin-bottom: 8px; | |
| 2003 | + font-weight: 600; | |
| 2004 | + color: #303133; | |
| 2005 | + font-size: 13px; | |
| 2006 | +} | |
| 2007 | + | |
| 2008 | +.sn-popover .sn-popover-list { | |
| 2009 | + max-height: 240px; | |
| 2010 | + overflow-y: auto; | |
| 2011 | + display: flex; | |
| 2012 | + flex-wrap: wrap; | |
| 2013 | + gap: 4px; | |
| 2014 | + padding: 2px; | |
| 2015 | +} | |
| 2016 | + | |
| 2017 | +.sn-popover .sn-tag { | |
| 2018 | + max-width: 100%; | |
| 2019 | +} | |
| 2020 | +</style> | |
| 2021 | + | ... | ... |
Antis.Erp.Plat/douyin/frontend/src/views/OrderListView.vue
| ... | ... | @@ -171,7 +171,7 @@ |
| 171 | 171 | @selection-change="handleSelectionChange" |
| 172 | 172 | @sort-change="handleSortChange" |
| 173 | 173 | > |
| 174 | - <el-table-column type="selection" width="55" :selectable="(row: Order) => (row.status === 0 || row.status === 1) && !(row.mergedOrderIds && row.mergedOrderIds.length > 1)" /> | |
| 174 | + <el-table-column type="selection" width="55" :selectable="(row: Order) => row.status === 0 || row.status === 1" /> | |
| 175 | 175 | <el-table-column label="来源店铺" width="120" show-overflow-tooltip> |
| 176 | 176 | <template #default="{ row }"> |
| 177 | 177 | <span style="font-size: 12px;">{{ row.shopName || '—' }}</span> |
| ... | ... | @@ -185,7 +185,16 @@ |
| 185 | 185 | <el-table-column prop="orderId" label="订单编号" min-width="300" show-overflow-tooltip> |
| 186 | 186 | <template #default="{ row }"> |
| 187 | 187 | <div v-if="row.mergedOrderIds && row.mergedOrderIds.length > 1" style="line-height: 1.5;"> |
| 188 | - <el-tag type="info" size="small" style="margin-bottom: 4px;">合并{{ row.mergedOrderIds.length }}单</el-tag> | |
| 188 | + <div style="display: flex; align-items: center; gap: 6px; margin-bottom: 4px;"> | |
| 189 | + <el-tag type="info" size="small">合并{{ row.mergedOrderIds.length }}单</el-tag> | |
| 190 | + <el-button | |
| 191 | + v-if="row.status === 0" | |
| 192 | + type="warning" | |
| 193 | + size="small" | |
| 194 | + link | |
| 195 | + @click="handleSplitMerged(row)" | |
| 196 | + >拆分</el-button> | |
| 197 | + </div> | |
| 189 | 198 | <div v-for="(num, i) in (row.mergedOrderNumbers || [row.orderId])" :key="i" style="font-size: 12px; word-break: break-all; display: flex; align-items: center; gap: 4px; margin-bottom: 2px;"> |
| 190 | 199 | <span>{{ num }}</span> |
| 191 | 200 | <el-tag |
| ... | ... | @@ -196,7 +205,13 @@ |
| 196 | 205 | >{{ getStatusText(row.mergedOrderStatuses[i]) }}</el-tag> |
| 197 | 206 | </div> |
| 198 | 207 | </div> |
| 199 | - <span v-else style="white-space: nowrap;">{{ row.orderId }}</span> | |
| 208 | + <div v-else style="line-height: 1.5;"> | |
| 209 | + <span style="white-space: nowrap;">{{ row.orderId }}</span> | |
| 210 | + <div v-if="row.noMerge && row.status === 0 && row.canUnsplit" style="margin-top: 3px; display: flex; align-items: center; gap: 6px;"> | |
| 211 | + <el-tag type="warning" size="small">已拆分</el-tag> | |
| 212 | + <el-button type="primary" size="small" link @click="handleUnsplit(row)">恢复合并</el-button> | |
| 213 | + </div> | |
| 214 | + </div> | |
| 200 | 215 | </template> |
| 201 | 216 | </el-table-column> |
| 202 | 217 | <el-table-column prop="status" label="状态" width="170" sortable="custom"> |
| ... | ... | @@ -345,13 +360,13 @@ |
| 345 | 360 | </template> |
| 346 | 361 | </el-table-column> |
| 347 | 362 | <!-- 备注(合并买家留言和卖家备注) --> |
| 348 | - <el-table-column label="备注" min-width="140" show-overflow-tooltip> | |
| 363 | + <el-table-column label="备注" min-width="160"> | |
| 349 | 364 | <template #default="{ row }"> |
| 350 | 365 | <div style="font-size: 11px; line-height: 1.5;"> |
| 351 | - <div v-if="row.buyerWords" style="color: #409eff; margin-bottom: 3px;"> | |
| 366 | + <div v-if="row.buyerWords" style="color: #409eff; margin-bottom: 3px; white-space: pre-line;" :title="row.buyerWords"> | |
| 352 | 367 | <span style="color: #909399; font-size: 10px;">买家:</span>{{ row.buyerWords }} |
| 353 | 368 | </div> |
| 354 | - <div v-if="row.sellerWords" style="color: #67c23a;"> | |
| 369 | + <div v-if="row.sellerWords" style="color: #67c23a; white-space: pre-line;" :title="row.sellerWords"> | |
| 355 | 370 | <span style="color: #909399; font-size: 10px;">卖家:</span>{{ row.sellerWords }} |
| 356 | 371 | </div> |
| 357 | 372 | <div v-if="!row.buyerWords && !row.sellerWords" style="color: #909399;"> |
| ... | ... | @@ -449,7 +464,7 @@ |
| 449 | 464 | import { ref, onMounted } from 'vue' |
| 450 | 465 | import { useRouter } from 'vue-router' |
| 451 | 466 | import { ElMessage, ElMessageBox } from 'element-plus' |
| 452 | -import { getOrders, syncOrders, createWaybill, initDatabase, printWaybill, shipToDouyin, manualShip, createSalesOrder, clearAndResync, getShops, type Order, type Shop } from '@/api/order' | |
| 467 | +import { getOrders, syncOrders, createWaybill, initDatabase, printWaybill, shipToDouyin, manualShip, createSalesOrder, clearAndResync, getShops, splitMergedOrders, unsplitOrders, type Order, type Shop } from '@/api/order' | |
| 453 | 468 | import { getTokenByCredentials, getAuthorizeUrl } from '@/api/auth' |
| 454 | 469 | import { getBackendBaseUrl } from '@/utils/config' |
| 455 | 470 | |
| ... | ... | @@ -923,6 +938,47 @@ const handleBatchCreateWaybill = async () => { |
| 923 | 938 | } |
| 924 | 939 | } |
| 925 | 940 | |
| 941 | +// 拆分合并单:把一组合并订单重新拆开,分别发货 | |
| 942 | +const handleSplitMerged = async (row: Order) => { | |
| 943 | + const ids = (row.mergedOrderIds && row.mergedOrderIds.length > 1) ? row.mergedOrderIds : [row.id] | |
| 944 | + if (ids.length < 2) { | |
| 945 | + ElMessage.info('该行不是合并订单,无需拆分') | |
| 946 | + return | |
| 947 | + } | |
| 948 | + try { | |
| 949 | + await ElMessageBox.confirm( | |
| 950 | + `确定要拆分这 ${ids.length} 笔合并订单吗?\n\n拆分后,这些订单将分别单独发货,不会再按"同买家+同地址"自动合并。如需恢复,可在"已拆分"标签旁点击"恢复合并"。`, | |
| 951 | + '拆分合并单', | |
| 952 | + { confirmButtonText: '确定拆分', cancelButtonText: '取消', type: 'warning' } | |
| 953 | + ) | |
| 954 | + } catch { | |
| 955 | + return | |
| 956 | + } | |
| 957 | + try { | |
| 958 | + loading.value = true | |
| 959 | + const { data } = await splitMergedOrders(ids) | |
| 960 | + ElMessage.success(data?.message || `已拆分 ${ids.length} 笔订单`) | |
| 961 | + await fetchOrders() | |
| 962 | + fetchStats() | |
| 963 | + } catch (err: any) { | |
| 964 | + ElMessage.error(err?.response?.data?.message || err?.message || '拆分失败') | |
| 965 | + } finally { | |
| 966 | + loading.value = false | |
| 967 | + } | |
| 968 | +} | |
| 969 | + | |
| 970 | +// 恢复合并:取消"拆分"标记,下次列表会重新自动合并 | |
| 971 | +const handleUnsplit = async (row: Order) => { | |
| 972 | + try { | |
| 973 | + const { data } = await unsplitOrders([row.id]) | |
| 974 | + ElMessage.success(data?.message || '已恢复合并') | |
| 975 | + await fetchOrders() | |
| 976 | + fetchStats() | |
| 977 | + } catch (err: any) { | |
| 978 | + ElMessage.error(err?.response?.data?.message || err?.message || '恢复合并失败') | |
| 979 | + } | |
| 980 | +} | |
| 981 | + | |
| 926 | 982 | // 创建运单(合并单跳转到合并编辑页;单笔订单走原有流程) |
| 927 | 983 | const handleCreateWaybill = async (order: Order) => { |
| 928 | 984 | if (order.status === 2 || order.status === 3 || order.status === 4) { | ... | ... |
Antis.Erp.Plat/netcore/src/Application/NCC.API/appsettings.json
| ... | ... | @@ -183,7 +183,7 @@ |
| 183 | 183 | "NCC_App": { |
| 184 | 184 | "CodeAreasName": "SubDev,Food,Extend,test", |
| 185 | 185 | //系统文件路径(末尾必须带斜杆) |
| 186 | - "SystemPath": "/Users/hexiaodong/Desktop/git/erp2025/Antis.Erp.Plat/netcore/uu-resources/", | |
| 186 | + "SystemPath": "/Users/mr.wang/代码库/file", | |
| 187 | 187 | //微信公众号允许上传文件类型 |
| 188 | 188 | "MPUploadFileType": "bmp,png,jpeg,jpg,gif,mp3,wma,wav,amr,mp4", |
| 189 | 189 | //微信允许上传文件类型 | ... | ... |
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtXsckdCrInput.cs
| ... | ... | @@ -32,6 +32,11 @@ namespace NCC.Extend.Entitys.Dto.WtXsckd |
| 32 | 32 | /// 经手人 |
| 33 | 33 | /// </summary> |
| 34 | 34 | public string jsr { get; set; } |
| 35 | + | |
| 36 | + /// <summary> | |
| 37 | + /// 发货人(抖音销售出库单必填;存用户 Id) | |
| 38 | + /// </summary> | |
| 39 | + public string fhr { get; set; } | |
| 35 | 40 | |
| 36 | 41 | /// <summary> |
| 37 | 42 | /// 原价金额(订单原价,与收银台一致) | ... | ... |
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtXsckdInfoOutput.cs
| ... | ... | @@ -37,6 +37,11 @@ namespace NCC.Extend.Entitys.Dto.WtXsckd |
| 37 | 37 | /// 经手人 |
| 38 | 38 | /// </summary> |
| 39 | 39 | public string jsr { get; set; } |
| 40 | + | |
| 41 | + /// <summary> | |
| 42 | + /// 发货人(详情展示 RealName;抖音销售出库单专用) | |
| 43 | + /// </summary> | |
| 44 | + public string fhr { get; set; } | |
| 40 | 45 | |
| 41 | 46 | /// <summary> |
| 42 | 47 | /// 原价金额(订单原价) | ... | ... |
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtXsckdListOutput.cs
| ... | ... | @@ -32,6 +32,11 @@ namespace NCC.Extend.Entitys.Dto.WtXsckd |
| 32 | 32 | /// 经手人 |
| 33 | 33 | /// </summary> |
| 34 | 34 | public string jsr { get; set; } |
| 35 | + | |
| 36 | + /// <summary> | |
| 37 | + /// 发货人(已转为 RealName 展示;抖音销售出库单专用) | |
| 38 | + /// </summary> | |
| 39 | + public string fhr { get; set; } | |
| 35 | 40 | |
| 36 | 41 | /// <summary> |
| 37 | 42 | /// 原价金额(订单原价) |
| ... | ... | @@ -133,6 +138,16 @@ namespace NCC.Extend.Entitys.Dto.WtXsckd |
| 133 | 138 | /// 收银批次号(同一次结账多单关联) |
| 134 | 139 | /// </summary> |
| 135 | 140 | public string sy_pch { get; set; } |
| 141 | + | |
| 142 | + /// <summary> | |
| 143 | + /// 物流运单号 | |
| 144 | + /// </summary> | |
| 145 | + public string yddh { get; set; } | |
| 146 | + | |
| 147 | + /// <summary> | |
| 148 | + /// 抖音订单号 | |
| 149 | + /// </summary> | |
| 150 | + public string dyddh { get; set; } | |
| 136 | 151 | |
| 137 | 152 | /// <summary> |
| 138 | 153 | /// 关联退货单单号(销售/预售出库单:逗号分隔的退货单 F_Id) | ... | ... |
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/WtDyDpszEntity.cs
| ... | ... | @@ -38,9 +38,77 @@ namespace NCC.Extend.Entitys |
| 38 | 38 | public string Skzh { get; set; } |
| 39 | 39 | |
| 40 | 40 | /// <summary> |
| 41 | + /// 出库仓库ID(抖音发货单专用,不再使用全局默认仓库) | |
| 42 | + /// </summary> | |
| 43 | + [SugarColumn(ColumnName = "F_Ck", IsNullable = true)] | |
| 44 | + public string Ck { get; set; } | |
| 45 | + | |
| 46 | + /// <summary> | |
| 41 | 47 | /// 备注 |
| 42 | 48 | /// </summary> |
| 43 | 49 | [SugarColumn(ColumnName = "F_Bz", Length = 500, IsNullable = true)] |
| 44 | 50 | public string Bz { get; set; } |
| 51 | + | |
| 52 | + /// <summary> | |
| 53 | + /// 抖音开放平台 AppKey | |
| 54 | + /// </summary> | |
| 55 | + [SugarColumn(ColumnName = "F_AppKey", Length = 100, IsNullable = true)] | |
| 56 | + public string AppKey { get; set; } | |
| 57 | + | |
| 58 | + /// <summary> | |
| 59 | + /// 抖音开放平台 AppSecret | |
| 60 | + /// </summary> | |
| 61 | + [SugarColumn(ColumnName = "F_AppSecret", Length = 200, IsNullable = true)] | |
| 62 | + public string AppSecret { get; set; } | |
| 63 | + | |
| 64 | + /// <summary> | |
| 65 | + /// 授权回调地址 | |
| 66 | + /// </summary> | |
| 67 | + [SugarColumn(ColumnName = "F_CallbackUrl", Length = 300, IsNullable = true)] | |
| 68 | + public string CallbackUrl { get; set; } | |
| 69 | + | |
| 70 | + /// <summary> | |
| 71 | + /// API 基础地址,默认 https://openapi-fxg.jinritemai.com | |
| 72 | + /// </summary> | |
| 73 | + [SugarColumn(ColumnName = "F_ApiBaseUrl", Length = 200, IsNullable = true)] | |
| 74 | + public string ApiBaseUrl { get; set; } | |
| 75 | + | |
| 76 | + /// <summary> | |
| 77 | + /// 同步订单查询天数(默认 30 天) | |
| 78 | + /// </summary> | |
| 79 | + [SugarColumn(ColumnName = "F_SyncDays", IsNullable = true)] | |
| 80 | + public int? SyncDays { get; set; } | |
| 81 | + | |
| 82 | + /// <summary>发货人姓名</summary> | |
| 83 | + [SugarColumn(ColumnName = "F_SenderName", Length = 100, IsNullable = true)] | |
| 84 | + public string SenderName { get; set; } | |
| 85 | + | |
| 86 | + /// <summary>发货人电话</summary> | |
| 87 | + [SugarColumn(ColumnName = "F_SenderPhone", Length = 50, IsNullable = true)] | |
| 88 | + public string SenderPhone { get; set; } | |
| 89 | + | |
| 90 | + /// <summary>发货人详细地址</summary> | |
| 91 | + [SugarColumn(ColumnName = "F_SenderAddress", Length = 300, IsNullable = true)] | |
| 92 | + public string SenderAddress { get; set; } | |
| 93 | + | |
| 94 | + /// <summary>发货人省份</summary> | |
| 95 | + [SugarColumn(ColumnName = "F_SenderProvince", Length = 50, IsNullable = true)] | |
| 96 | + public string SenderProvince { get; set; } | |
| 97 | + | |
| 98 | + /// <summary>发货人城市</summary> | |
| 99 | + [SugarColumn(ColumnName = "F_SenderCity", Length = 50, IsNullable = true)] | |
| 100 | + public string SenderCity { get; set; } | |
| 101 | + | |
| 102 | + /// <summary>发货人区县</summary> | |
| 103 | + [SugarColumn(ColumnName = "F_SenderDistrict", Length = 50, IsNullable = true)] | |
| 104 | + public string SenderDistrict { get; set; } | |
| 105 | + | |
| 106 | + /// <summary>发货人街道</summary> | |
| 107 | + [SugarColumn(ColumnName = "F_SenderStreet", Length = 100, IsNullable = true)] | |
| 108 | + public string SenderStreet { get; set; } | |
| 109 | + | |
| 110 | + /// <summary>启用状态:1=启用,0=停用(停用后不再同步订单)</summary> | |
| 111 | + [SugarColumn(ColumnName = "F_Enabled", IsNullable = true)] | |
| 112 | + public int? Enabled { get; set; } | |
| 45 | 113 | } |
| 46 | 114 | } | ... | ... |
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/WtXsckdEntity.cs
| ... | ... | @@ -40,6 +40,12 @@ namespace NCC.Extend.Entitys |
| 40 | 40 | /// </summary> |
| 41 | 41 | [SugarColumn(ColumnName = "jsr")] |
| 42 | 42 | public string Jsr { get; set; } |
| 43 | + | |
| 44 | + /// <summary> | |
| 45 | + /// 发货人(抖音发货单提交销售出库时必填;独立于经手人。存 ERP 用户 Id) | |
| 46 | + /// </summary> | |
| 47 | + [SugarColumn(ColumnName = "fhr", IsNullable = true)] | |
| 48 | + public string Fhr { get; set; } | |
| 43 | 49 | |
| 44 | 50 | /// <summary> |
| 45 | 51 | /// 原价金额(订单原价,与收银台一致) | ... | ... |
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/BillSummary/WtBillSummaryService.cs
| ... | ... | @@ -218,15 +218,26 @@ namespace NCC.Extend.BillSummary |
| 218 | 218 | jsrDisp = jList[0] ?? ""; |
| 219 | 219 | } |
| 220 | 220 | |
| 221 | + // 与 GetInfo一致:gys 多为 wt_wldw 主键;历史可能为 wt_gys或已存展示名 | |
| 221 | 222 | string gysDisp = ""; |
| 222 | 223 | if (!string.IsNullOrWhiteSpace(xsckd.Gys)) |
| 223 | 224 | { |
| 224 | - var gList = await _db.Queryable<WtGysEntity>() | |
| 225 | - .Where(g => g.Id == xsckd.Gys) | |
| 226 | - .Select(g => g.Gysmc) | |
| 225 | + var rawGys = xsckd.Gys.Trim(); | |
| 226 | + var wldwMcList = await _db.Queryable<WtWldwEntity>() | |
| 227 | + .Where(w => w.Id == rawGys) | |
| 228 | + .Select(w => w.Dwmc) | |
| 227 | 229 | .ToListAsync(); |
| 228 | - if (gList.Count > 0) | |
| 229 | - gysDisp = gList[0] ?? ""; | |
| 230 | + if (wldwMcList.Count > 0 && !string.IsNullOrWhiteSpace(wldwMcList[0])) | |
| 231 | + gysDisp = wldwMcList[0]!.Trim(); | |
| 232 | + else | |
| 233 | + { | |
| 234 | + var gList = await _db.Queryable<WtGysEntity>() | |
| 235 | + .Where(g => g.Id == rawGys) | |
| 236 | + .Select(g => g.Gysmc) | |
| 237 | + .ToListAsync(); | |
| 238 | + var gysmc = gList.Count > 0 ? gList[0] : null; | |
| 239 | + gysDisp = string.IsNullOrWhiteSpace(gysmc) ? rawGys : gysmc.Trim(); | |
| 240 | + } | |
| 230 | 241 | } |
| 231 | 242 | |
| 232 | 243 | var (cjckDisp, rkckDisp) = await ResolveMainWarehouseDisplayAsync(xsckd.Cjck, xsckd.Rkck); | ... | ... |
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtDyDpszService.cs
| ... | ... | @@ -12,7 +12,7 @@ using Yitter.IdGenerator; |
| 12 | 12 | namespace NCC.Extend.WtDyDpsz |
| 13 | 13 | { |
| 14 | 14 | /// <summary> |
| 15 | - /// 抖音店铺设置服务(每个店铺对应的往来单位、收款账户) | |
| 15 | + /// 抖音店铺设置服务(每个店铺对应的抖音 API 配置、往来单位、收款账户、发货人等) | |
| 16 | 16 | /// </summary> |
| 17 | 17 | [ApiDescriptionSettings(Tag = "Extend", Name = "WtDyDpsz", Order = 200)] |
| 18 | 18 | [Route("api/Extend/[controller]")] |
| ... | ... | @@ -28,68 +28,149 @@ namespace NCC.Extend.WtDyDpsz |
| 28 | 28 | } |
| 29 | 29 | |
| 30 | 30 | /// <summary> |
| 31 | - /// 获取所有店铺设置 | |
| 31 | + /// 确保 wt_dy_dpsz 表结构与实体同步(首次调用会检查并补齐新增列)。 | |
| 32 | 32 | /// </summary> |
| 33 | - [HttpGet("")] | |
| 34 | - [AllowAnonymous] | |
| 35 | - public async Task<dynamic> GetList() | |
| 33 | + private static bool _dpszColsEnsured; | |
| 34 | + private static readonly object _dpszColsLock = new object(); | |
| 35 | + private void EnsureWtDyDpszColumns() | |
| 36 | 36 | { |
| 37 | - var list = await _db.Queryable<WtDyDpszEntity>().ToListAsync(); | |
| 38 | - return list.Select(e => new | |
| 37 | + if (_dpszColsEnsured) return; | |
| 38 | + lock (_dpszColsLock) | |
| 39 | + { | |
| 40 | + if (_dpszColsEnsured) return; | |
| 41 | + try | |
| 42 | + { | |
| 43 | + var cols = _db.Ado.SqlQuery<string>( | |
| 44 | + "SELECT LOWER(COLUMN_NAME) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wt_dy_dpsz'"); | |
| 45 | + var set = new HashSet<string>(cols ?? new List<string>()); | |
| 46 | + if (set.Count > 0) | |
| 47 | + { | |
| 48 | + // 旧环境兼容:按需补齐列 | |
| 49 | + if (!set.Contains("f_ck")) | |
| 50 | + _db.Ado.ExecuteCommand("ALTER TABLE `wt_dy_dpsz` ADD COLUMN `F_Ck` VARCHAR(64) NULL COMMENT '出库仓库ID(抖音发货单专用)'"); | |
| 51 | + if (!set.Contains("f_appkey")) | |
| 52 | + _db.Ado.ExecuteCommand("ALTER TABLE `wt_dy_dpsz` ADD COLUMN `F_AppKey` VARCHAR(100) NULL COMMENT '抖音 AppKey'"); | |
| 53 | + if (!set.Contains("f_appsecret")) | |
| 54 | + _db.Ado.ExecuteCommand("ALTER TABLE `wt_dy_dpsz` ADD COLUMN `F_AppSecret` VARCHAR(200) NULL COMMENT '抖音 AppSecret'"); | |
| 55 | + if (!set.Contains("f_callbackurl")) | |
| 56 | + _db.Ado.ExecuteCommand("ALTER TABLE `wt_dy_dpsz` ADD COLUMN `F_CallbackUrl` VARCHAR(300) NULL COMMENT '授权回调地址'"); | |
| 57 | + if (!set.Contains("f_apibaseurl")) | |
| 58 | + _db.Ado.ExecuteCommand("ALTER TABLE `wt_dy_dpsz` ADD COLUMN `F_ApiBaseUrl` VARCHAR(200) NULL COMMENT '抖音 API 基础地址'"); | |
| 59 | + if (!set.Contains("f_syncdays")) | |
| 60 | + _db.Ado.ExecuteCommand("ALTER TABLE `wt_dy_dpsz` ADD COLUMN `F_SyncDays` INT NULL COMMENT '同步订单查询天数'"); | |
| 61 | + if (!set.Contains("f_sendername")) | |
| 62 | + _db.Ado.ExecuteCommand("ALTER TABLE `wt_dy_dpsz` ADD COLUMN `F_SenderName` VARCHAR(100) NULL COMMENT '发货人姓名'"); | |
| 63 | + if (!set.Contains("f_senderphone")) | |
| 64 | + _db.Ado.ExecuteCommand("ALTER TABLE `wt_dy_dpsz` ADD COLUMN `F_SenderPhone` VARCHAR(50) NULL COMMENT '发货人电话'"); | |
| 65 | + if (!set.Contains("f_senderaddress")) | |
| 66 | + _db.Ado.ExecuteCommand("ALTER TABLE `wt_dy_dpsz` ADD COLUMN `F_SenderAddress` VARCHAR(300) NULL COMMENT '发货人地址'"); | |
| 67 | + if (!set.Contains("f_senderprovince")) | |
| 68 | + _db.Ado.ExecuteCommand("ALTER TABLE `wt_dy_dpsz` ADD COLUMN `F_SenderProvince` VARCHAR(50) NULL COMMENT '发货人省份'"); | |
| 69 | + if (!set.Contains("f_sendercity")) | |
| 70 | + _db.Ado.ExecuteCommand("ALTER TABLE `wt_dy_dpsz` ADD COLUMN `F_SenderCity` VARCHAR(50) NULL COMMENT '发货人城市'"); | |
| 71 | + if (!set.Contains("f_senderdistrict")) | |
| 72 | + _db.Ado.ExecuteCommand("ALTER TABLE `wt_dy_dpsz` ADD COLUMN `F_SenderDistrict` VARCHAR(50) NULL COMMENT '发货人区县'"); | |
| 73 | + if (!set.Contains("f_senderstreet")) | |
| 74 | + _db.Ado.ExecuteCommand("ALTER TABLE `wt_dy_dpsz` ADD COLUMN `F_SenderStreet` VARCHAR(100) NULL COMMENT '发货人街道'"); | |
| 75 | + if (!set.Contains("f_enabled")) | |
| 76 | + _db.Ado.ExecuteCommand("ALTER TABLE `wt_dy_dpsz` ADD COLUMN `F_Enabled` INT NULL DEFAULT 1 COMMENT '启用状态'"); | |
| 77 | + } | |
| 78 | + } | |
| 79 | + catch | |
| 80 | + { | |
| 81 | + // 忽略:表不存在或无权限;由 CodeFirst/迁移流程处理 | |
| 82 | + } | |
| 83 | + _dpszColsEnsured = true; | |
| 84 | + } | |
| 85 | + } | |
| 86 | + | |
| 87 | + /// <summary> | |
| 88 | + /// 转换为对外 DTO(前端/列表用,脱敏 AppSecret)。 | |
| 89 | + /// </summary> | |
| 90 | + private static object ToOutputDto(WtDyDpszEntity e, bool includeSecret) | |
| 91 | + { | |
| 92 | + return new | |
| 39 | 93 | { |
| 40 | 94 | id = e.Id, |
| 41 | 95 | shopId = e.ShopId, |
| 42 | 96 | shopName = e.ShopName ?? "", |
| 43 | 97 | kh = e.Kh ?? "", |
| 44 | 98 | skzh = e.Skzh ?? "", |
| 45 | - bz = e.Bz ?? "" | |
| 46 | - }); | |
| 99 | + ck = e.Ck ?? "", | |
| 100 | + bz = e.Bz ?? "", | |
| 101 | + appKey = e.AppKey ?? "", | |
| 102 | + // 列表页默认不回显 AppSecret,编辑时才返回 | |
| 103 | + appSecret = includeSecret ? (e.AppSecret ?? "") : "", | |
| 104 | + callbackUrl = e.CallbackUrl ?? "", | |
| 105 | + apiBaseUrl = e.ApiBaseUrl ?? "", | |
| 106 | + syncDays = e.SyncDays ?? 30, | |
| 107 | + senderName = e.SenderName ?? "", | |
| 108 | + senderPhone = e.SenderPhone ?? "", | |
| 109 | + senderAddress = e.SenderAddress ?? "", | |
| 110 | + senderProvince = e.SenderProvince ?? "", | |
| 111 | + senderCity = e.SenderCity ?? "", | |
| 112 | + senderDistrict = e.SenderDistrict ?? "", | |
| 113 | + senderStreet = e.SenderStreet ?? "", | |
| 114 | + enabled = e.Enabled ?? 1 | |
| 115 | + }; | |
| 47 | 116 | } |
| 48 | 117 | |
| 49 | 118 | /// <summary> |
| 50 | - /// 根据店铺ID获取设置 | |
| 119 | + /// 获取所有店铺设置(列表用,AppSecret 脱敏) | |
| 120 | + /// </summary> | |
| 121 | + [HttpGet("")] | |
| 122 | + [AllowAnonymous] | |
| 123 | + public async Task<dynamic> GetList() | |
| 124 | + { | |
| 125 | + EnsureWtDyDpszColumns(); | |
| 126 | + var list = await _db.Queryable<WtDyDpszEntity>().ToListAsync(); | |
| 127 | + return list.Select(e => ToOutputDto(e, includeSecret: false)); | |
| 128 | + } | |
| 129 | + | |
| 130 | + /// <summary> | |
| 131 | + /// 内部接口:抖音后端启动/刷新时拉取全部启用店铺的完整配置(含 AppSecret) | |
| 132 | + /// </summary> | |
| 133 | + [HttpGet("internal/all")] | |
| 134 | + [AllowAnonymous] | |
| 135 | + public async Task<dynamic> GetInternalAll() | |
| 136 | + { | |
| 137 | + EnsureWtDyDpszColumns(); | |
| 138 | + var list = await _db.Queryable<WtDyDpszEntity>() | |
| 139 | + .Where(e => e.Enabled == null || e.Enabled == 1) | |
| 140 | + .ToListAsync(); | |
| 141 | + return list.Select(e => ToOutputDto(e, includeSecret: true)); | |
| 142 | + } | |
| 143 | + | |
| 144 | + /// <summary> | |
| 145 | + /// 根据店铺ID获取设置(供抖音发货单按店铺读取往来单位/收款账户/仓库) | |
| 51 | 146 | /// </summary> |
| 52 | 147 | [HttpGet("by-shop/{shopId}")] |
| 53 | 148 | [AllowAnonymous] |
| 54 | 149 | public async Task<dynamic> GetByShopId(long shopId) |
| 55 | 150 | { |
| 151 | + EnsureWtDyDpszColumns(); | |
| 56 | 152 | var entity = await _db.Queryable<WtDyDpszEntity>() |
| 57 | 153 | .Where(e => e.ShopId == shopId) |
| 58 | 154 | .FirstAsync(); |
| 59 | 155 | if (entity == null) |
| 60 | 156 | return null; |
| 61 | - return new | |
| 62 | - { | |
| 63 | - id = entity.Id, | |
| 64 | - shopId = entity.ShopId, | |
| 65 | - shopName = entity.ShopName ?? "", | |
| 66 | - kh = entity.Kh ?? "", | |
| 67 | - skzh = entity.Skzh ?? "", | |
| 68 | - bz = entity.Bz ?? "" | |
| 69 | - }; | |
| 157 | + return ToOutputDto(entity, includeSecret: false); | |
| 70 | 158 | } |
| 71 | 159 | |
| 72 | 160 | /// <summary> |
| 73 | - /// 获取单条 | |
| 161 | + /// 获取单条(编辑详情,返回 AppSecret) | |
| 74 | 162 | /// </summary> |
| 75 | 163 | [HttpGet("{id}")] |
| 76 | 164 | [AllowAnonymous] |
| 77 | 165 | public async Task<dynamic> GetInfo(string id) |
| 78 | 166 | { |
| 167 | + EnsureWtDyDpszColumns(); | |
| 79 | 168 | var entity = await _db.Queryable<WtDyDpszEntity>() |
| 80 | 169 | .Where(e => e.Id == id) |
| 81 | 170 | .FirstAsync(); |
| 82 | 171 | if (entity == null) |
| 83 | 172 | return null; |
| 84 | - return new | |
| 85 | - { | |
| 86 | - id = entity.Id, | |
| 87 | - shopId = entity.ShopId, | |
| 88 | - shopName = entity.ShopName ?? "", | |
| 89 | - kh = entity.Kh ?? "", | |
| 90 | - skzh = entity.Skzh ?? "", | |
| 91 | - bz = entity.Bz ?? "" | |
| 92 | - }; | |
| 173 | + return ToOutputDto(entity, includeSecret: true); | |
| 93 | 174 | } |
| 94 | 175 | |
| 95 | 176 | /// <summary> |
| ... | ... | @@ -99,6 +180,7 @@ namespace NCC.Extend.WtDyDpsz |
| 99 | 180 | [AllowAnonymous] |
| 100 | 181 | public async Task<dynamic> Create([FromBody] WtDyDpszInput input) |
| 101 | 182 | { |
| 183 | + EnsureWtDyDpszColumns(); | |
| 102 | 184 | var exists = await _db.Queryable<WtDyDpszEntity>() |
| 103 | 185 | .Where(e => e.ShopId == input.ShopId) |
| 104 | 186 | .FirstAsync(); |
| ... | ... | @@ -108,12 +190,8 @@ namespace NCC.Extend.WtDyDpsz |
| 108 | 190 | var entity = new WtDyDpszEntity |
| 109 | 191 | { |
| 110 | 192 | Id = YitIdHelper.NextId().ToString(), |
| 111 | - ShopId = input.ShopId, | |
| 112 | - ShopName = input.ShopName ?? "", | |
| 113 | - Kh = input.Kh ?? "", | |
| 114 | - Skzh = input.Skzh ?? "", | |
| 115 | - Bz = input.Bz ?? "" | |
| 116 | 193 | }; |
| 194 | + ApplyInputToEntity(input, entity, isCreate: true); | |
| 117 | 195 | await _db.Insertable(entity).ExecuteCommandAsync(); |
| 118 | 196 | return new { msg = "保存成功", id = entity.Id }; |
| 119 | 197 | } |
| ... | ... | @@ -125,17 +203,14 @@ namespace NCC.Extend.WtDyDpsz |
| 125 | 203 | [AllowAnonymous] |
| 126 | 204 | public async Task<dynamic> Update(string id, [FromBody] WtDyDpszInput input) |
| 127 | 205 | { |
| 206 | + EnsureWtDyDpszColumns(); | |
| 128 | 207 | var entity = await _db.Queryable<WtDyDpszEntity>() |
| 129 | 208 | .Where(e => e.Id == id) |
| 130 | 209 | .FirstAsync(); |
| 131 | 210 | if (entity == null) |
| 132 | 211 | return null; |
| 133 | 212 | |
| 134 | - entity.ShopId = input.ShopId; | |
| 135 | - entity.ShopName = input.ShopName ?? ""; | |
| 136 | - entity.Kh = input.Kh ?? ""; | |
| 137 | - entity.Skzh = input.Skzh ?? ""; | |
| 138 | - entity.Bz = input.Bz ?? ""; | |
| 213 | + ApplyInputToEntity(input, entity, isCreate: false); | |
| 139 | 214 | await _db.Updateable(entity).ExecuteCommandAsync(); |
| 140 | 215 | return new { msg = "保存成功" }; |
| 141 | 216 | } |
| ... | ... | @@ -150,6 +225,33 @@ namespace NCC.Extend.WtDyDpsz |
| 150 | 225 | await _db.Deleteable<WtDyDpszEntity>().Where(e => e.Id == id).ExecuteCommandAsync(); |
| 151 | 226 | return new { msg = "删除成功" }; |
| 152 | 227 | } |
| 228 | + | |
| 229 | + private static void ApplyInputToEntity(WtDyDpszInput input, WtDyDpszEntity entity, bool isCreate) | |
| 230 | + { | |
| 231 | + entity.ShopId = input.ShopId; | |
| 232 | + entity.ShopName = input.ShopName ?? ""; | |
| 233 | + entity.Kh = input.Kh ?? ""; | |
| 234 | + entity.Skzh = input.Skzh ?? ""; | |
| 235 | + entity.Ck = input.Ck ?? ""; | |
| 236 | + entity.Bz = input.Bz ?? ""; | |
| 237 | + entity.AppKey = input.AppKey ?? ""; | |
| 238 | + // 编辑时如未传 AppSecret,保留数据库中的原值,避免 UI 未回显时误清空 | |
| 239 | + if (isCreate || !string.IsNullOrEmpty(input.AppSecret)) | |
| 240 | + entity.AppSecret = input.AppSecret ?? ""; | |
| 241 | + entity.CallbackUrl = input.CallbackUrl ?? ""; | |
| 242 | + entity.ApiBaseUrl = string.IsNullOrWhiteSpace(input.ApiBaseUrl) | |
| 243 | + ? "https://openapi-fxg.jinritemai.com" | |
| 244 | + : input.ApiBaseUrl; | |
| 245 | + entity.SyncDays = input.SyncDays > 0 ? input.SyncDays : 30; | |
| 246 | + entity.SenderName = input.SenderName ?? ""; | |
| 247 | + entity.SenderPhone = input.SenderPhone ?? ""; | |
| 248 | + entity.SenderAddress = input.SenderAddress ?? ""; | |
| 249 | + entity.SenderProvince = input.SenderProvince ?? ""; | |
| 250 | + entity.SenderCity = input.SenderCity ?? ""; | |
| 251 | + entity.SenderDistrict = input.SenderDistrict ?? ""; | |
| 252 | + entity.SenderStreet = input.SenderStreet ?? ""; | |
| 253 | + entity.Enabled = input.Enabled.HasValue ? input.Enabled.Value : 1; | |
| 254 | + } | |
| 153 | 255 | } |
| 154 | 256 | |
| 155 | 257 | public class WtDyDpszInput |
| ... | ... | @@ -158,6 +260,30 @@ namespace NCC.Extend.WtDyDpsz |
| 158 | 260 | public string ShopName { get; set; } |
| 159 | 261 | public string Kh { get; set; } |
| 160 | 262 | public string Skzh { get; set; } |
| 263 | + /// <summary>出库仓库ID(抖音发货单专用)</summary> | |
| 264 | + public string Ck { get; set; } | |
| 161 | 265 | public string Bz { get; set; } |
| 266 | + | |
| 267 | + /// <summary>抖音 AppKey</summary> | |
| 268 | + public string AppKey { get; set; } | |
| 269 | + /// <summary>抖音 AppSecret(编辑时留空表示不修改)</summary> | |
| 270 | + public string AppSecret { get; set; } | |
| 271 | + /// <summary>授权回调地址</summary> | |
| 272 | + public string CallbackUrl { get; set; } | |
| 273 | + /// <summary>API 基础地址</summary> | |
| 274 | + public string ApiBaseUrl { get; set; } | |
| 275 | + /// <summary>同步订单查询天数</summary> | |
| 276 | + public int SyncDays { get; set; } | |
| 277 | + | |
| 278 | + public string SenderName { get; set; } | |
| 279 | + public string SenderPhone { get; set; } | |
| 280 | + public string SenderAddress { get; set; } | |
| 281 | + public string SenderProvince { get; set; } | |
| 282 | + public string SenderCity { get; set; } | |
| 283 | + public string SenderDistrict { get; set; } | |
| 284 | + public string SenderStreet { get; set; } | |
| 285 | + | |
| 286 | + /// <summary>启用状态:1=启用,0=停用</summary> | |
| 287 | + public int? Enabled { get; set; } | |
| 162 | 288 | } |
| 163 | 289 | } | ... | ... |
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtSpService.cs
| ... | ... | @@ -756,6 +756,30 @@ CREATE TABLE `wt_sp_dysku` ( |
| 756 | 756 | } |
| 757 | 757 | |
| 758 | 758 | /// <summary> |
| 759 | + /// 商品编码唯一性检查:返回该编码是否已被其它商品占用 | |
| 760 | + /// </summary> | |
| 761 | + /// <param name="spbm">待校验的商品编码(前后空白会自动 trim;空值视为"未填",返回 exists=false)</param> | |
| 762 | + /// <param name="id">编辑场景下传入当前商品主键,避免把自己当作冲突</param> | |
| 763 | + /// <returns><c>exists</c> 为 true 时表示已被其他商品占用,<c>id</c>/<c>spmc</c> 给出占用方,用于前端提示</returns> | |
| 764 | + [HttpGet("Actions/CheckSpbm")] | |
| 765 | + public async Task<dynamic> CheckSpbm([FromQuery] string spbm, [FromQuery] string id) | |
| 766 | + { | |
| 767 | + var code = spbm?.Trim(); | |
| 768 | + if (string.IsNullOrEmpty(code)) | |
| 769 | + return new { exists = false }; | |
| 770 | + | |
| 771 | + var selfId = id?.Trim(); | |
| 772 | + var dup = await _db.Queryable<WtSpEntity>() | |
| 773 | + .Where(p => p.Spbm == code) | |
| 774 | + .WhereIF(!string.IsNullOrEmpty(selfId), p => p.Id != selfId) | |
| 775 | + .Select(p => new { p.Id, p.Spmc }) | |
| 776 | + .FirstAsync(); | |
| 777 | + | |
| 778 | + if (dup == null) return new { exists = false }; | |
| 779 | + return new { exists = true, id = dup.Id, spmc = dup.Spmc }; | |
| 780 | + } | |
| 781 | + | |
| 782 | + /// <summary> | |
| 759 | 783 | /// 一次性修复 wt_sp_cost 历史数据:ck 存仓库名称的改为仓库 ID;F_Id 为「商品_仓库」等非雪花主键的改为 YitId。 |
| 760 | 784 | /// </summary> |
| 761 | 785 | private void MigrateSpCostCkToWarehouseId() |
| ... | ... | @@ -1101,10 +1125,23 @@ CREATE TABLE `wt_sp_dysku` ( |
| 1101 | 1125 | EnsureWtSpDyskuTable(); |
| 1102 | 1126 | var mergedSkus = MergeDyspidInputs(input.dyspid, input.dyspidList); |
| 1103 | 1127 | |
| 1128 | + // 商品编码唯一性校验:非空时 wt_sp.F_Spbm 不允许重复 | |
| 1129 | + var newSpbm = input.spbm?.Trim(); | |
| 1130 | + if (!string.IsNullOrEmpty(newSpbm)) | |
| 1131 | + { | |
| 1132 | + var dup = await _db.Queryable<WtSpEntity>() | |
| 1133 | + .Where(p => p.Spbm == newSpbm) | |
| 1134 | + .Select(p => new { p.Id, p.Spmc }) | |
| 1135 | + .FirstAsync(); | |
| 1136 | + if (dup != null) | |
| 1137 | + throw NCCException.Oh($"商品编码「{newSpbm}」已存在(商品:{dup.Spmc}),请更换编码"); | |
| 1138 | + } | |
| 1139 | + | |
| 1104 | 1140 | var entity = input.Adapt<WtSpEntity>(); |
| 1105 | 1141 | entity.Id = YitIdHelper.NextId().ToString(); |
| 1106 | 1142 | entity.Pl = NormalizePlCsv(entity.Pl); |
| 1107 | 1143 | entity.Kc = 0; |
| 1144 | + if (!string.IsNullOrEmpty(newSpbm)) entity.Spbm = newSpbm; | |
| 1108 | 1145 | entity.Dyspid = mergedSkus.Count > 0 ? mergedSkus[0] : null; |
| 1109 | 1146 | |
| 1110 | 1147 | _db.BeginTran(); |
| ... | ... | @@ -1334,9 +1371,22 @@ CREATE TABLE `wt_sp_dysku` ( |
| 1334 | 1371 | // 添加调试信息 |
| 1335 | 1372 | Console.WriteLine($"更新商品 - ID: {id}"); |
| 1336 | 1373 | |
| 1374 | + // 商品编码唯一性校验:非空时排除自身后不允许重复 | |
| 1375 | + var newSpbm = input.spbm?.Trim(); | |
| 1376 | + if (!string.IsNullOrEmpty(newSpbm)) | |
| 1377 | + { | |
| 1378 | + var dup = await _db.Queryable<WtSpEntity>() | |
| 1379 | + .Where(p => p.Spbm == newSpbm && p.Id != id) | |
| 1380 | + .Select(p => new { p.Id, p.Spmc }) | |
| 1381 | + .FirstAsync(); | |
| 1382 | + if (dup != null) | |
| 1383 | + throw NCCException.Oh($"商品编码「{newSpbm}」已被其他商品使用(商品:{dup.Spmc}),请更换编码"); | |
| 1384 | + } | |
| 1385 | + | |
| 1337 | 1386 | var entity = input.Adapt<WtSpEntity>(); |
| 1338 | 1387 | entity.Pl = NormalizePlCsv(entity.Pl); |
| 1339 | 1388 | entity.Id = id; |
| 1389 | + if (!string.IsNullOrEmpty(newSpbm)) entity.Spbm = newSpbm; | |
| 1340 | 1390 | entity.Dyspid = mergedSkus.Count > 0 ? mergedSkus[0] : null; |
| 1341 | 1391 | |
| 1342 | 1392 | _db.BeginTran(); | ... | ... |
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtTjdService.cs
| ... | ... | @@ -299,7 +299,7 @@ namespace NCC.Extend.WtTjd |
| 299 | 299 | } |
| 300 | 300 | |
| 301 | 301 | /// <summary> |
| 302 | - /// 反审调价单:将商品档案零售价(<c>wt_sp.F_Lsj</c>)恢复为明细中的调前单价,状态回退为草稿 | |
| 302 | + /// 反审调价单:将商品成本(<c>wt_sp_cost.cbj</c>,按本单仓库 + 商品定位)恢复为明细中的调前单价,状态回退为草稿 | |
| 303 | 303 | /// </summary> |
| 304 | 304 | /// <param name="id">单据编号</param> |
| 305 | 305 | /// <returns></returns> |
| ... | ... | @@ -322,9 +322,13 @@ namespace NCC.Extend.WtTjd |
| 322 | 322 | |
| 323 | 323 | foreach (var mx in mxList) |
| 324 | 324 | { |
| 325 | - await _db.Updateable<WtSpEntity>() | |
| 326 | - .SetColumns(it => new WtSpEntity { Lsj = mx.Tqdj }) | |
| 327 | - .Where(it => it.Id == mx.Spbh) | |
| 325 | + var ckId = !string.IsNullOrEmpty(mx.Ck) ? mx.Ck : entity.Ck; | |
| 326 | + if (string.IsNullOrEmpty(ckId) || string.IsNullOrEmpty(mx.Spbh)) continue; | |
| 327 | + | |
| 328 | + var tqdj = mx.Tqdj; | |
| 329 | + await _db.Updateable<WtSpCostEntity>() | |
| 330 | + .SetColumns(it => new WtSpCostEntity { Cbj = tqdj }) | |
| 331 | + .Where(it => it.Spbh == mx.Spbh && it.Ck == ckId) | |
| 328 | 332 | .ExecuteCommandAsync(); |
| 329 | 333 | } |
| 330 | 334 | |
| ... | ... | @@ -344,10 +348,10 @@ namespace NCC.Extend.WtTjd |
| 344 | 348 | } |
| 345 | 349 | |
| 346 | 350 | /// <summary> |
| 347 | - /// 按仓库加载商品列表(含库存、档案零售价 <c>tqdj</c>、成本参考 <c>cbj</c>) | |
| 351 | + /// 按仓库加载商品列表(含库存、调前成本 <c>tqdj</c>) | |
| 348 | 352 | /// </summary> |
| 349 | 353 | /// <param name="ck">仓库ID</param> |
| 350 | - /// <returns>商品列表;<c>tqdj</c> 为 <c>wt_sp.F_Lsj</c>(调价基准),与「加载全部商品」口径一致</returns> | |
| 354 | + /// <returns>商品列表;<c>tqdj</c> 为当前成本价 <c>wt_sp_cost.cbj</c>(调价基准为"成本"而非零售价)</returns> | |
| 351 | 355 | [HttpGet("Actions/LoadProducts")] |
| 352 | 356 | public async Task<dynamic> LoadProducts([FromQuery] string ck) |
| 353 | 357 | { |
| ... | ... | @@ -355,9 +359,10 @@ namespace NCC.Extend.WtTjd |
| 355 | 359 | throw NCCException.Bah("仓库ID不能为空"); |
| 356 | 360 | |
| 357 | 361 | // 库存数量仅以 wt_sp_cost.sl 为准(流水账);无成本行或 sl=0 则不出现,不再用序列号条数兜底 |
| 362 | + // tqdj(调前单价)= wt_sp_cost.cbj(成本价),调价单调的是"成本",不是零售价 | |
| 358 | 363 | var products = await _db.Ado.SqlQueryAsync<dynamic>( |
| 359 | 364 | @"SELECT sc.spbh, sp.F_Spbm as spbm, sp.F_Spmc as spmc, sc.sl, |
| 360 | - IFNULL(sp.F_Lsj, 0) as tqdj, IFNULL(sc.cbj, 0) as cbj | |
| 365 | + IFNULL(sc.cbj, 0) as tqdj, IFNULL(sc.cbj, 0) as cbj | |
| 361 | 366 | FROM wt_sp_cost sc |
| 362 | 367 | INNER JOIN wt_sp sp ON sc.spbh = sp.F_Id |
| 363 | 368 | WHERE sc.ck = @ck AND sc.sl > 0 |
| ... | ... | @@ -494,7 +499,7 @@ namespace NCC.Extend.WtTjd |
| 494 | 499 | |
| 495 | 500 | /// <summary> |
| 496 | 501 | /// 加载某仓库曾在序列号表出现过的商品(按 <c>spbh</c> 聚合一条);在库数量 <c>sl</c> 取 <c>wt_sp_cost.sl</c>(流水账),不再按序列号条数统计。 |
| 497 | - /// <c>tqdj</c> 为档案零售价 <c>wt_sp.F_Lsj</c>;<c>cbj</c> 为该仓成本单价。 | |
| 502 | + /// <c>tqdj</c> 为该仓调前成本 <c>wt_sp_cost.cbj</c>(调价单调的是成本,不再取零售价)。 | |
| 498 | 503 | /// </summary> |
| 499 | 504 | /// <remarks> |
| 500 | 505 | /// 禁止按 <c>sn.spmc</c> 分组,否则同一商品因序列号表名称空/不一致会拆成多行。 |
| ... | ... | @@ -507,10 +512,11 @@ namespace NCC.Extend.WtTjd |
| 507 | 512 | if (string.IsNullOrEmpty(ck)) |
| 508 | 513 | throw NCCException.Bah("仓库ID不能为空"); |
| 509 | 514 | |
| 515 | + // tqdj(调前单价)= 该仓 wt_sp_cost.cbj(成本价) | |
| 510 | 516 | var products = await _db.Ado.SqlQueryAsync<dynamic>( |
| 511 | 517 | @"SELECT sn.spbh, MAX(sp.F_Spbm) as spbm, MAX(sp.F_Spmc) as spmc, |
| 512 | 518 | IFNULL(MAX(sc.sl), 0) as sl, |
| 513 | - IFNULL(MAX(sp.F_Lsj), 0) as tqdj, IFNULL(MAX(sc.cbj), 0) as cbj | |
| 519 | + IFNULL(MAX(sc.cbj), 0) as tqdj, IFNULL(MAX(sc.cbj), 0) as cbj | |
| 514 | 520 | FROM wt_serial_number sn |
| 515 | 521 | LEFT JOIN wt_sp sp ON sn.spbh = sp.F_Id |
| 516 | 522 | LEFT JOIN wt_sp_cost sc ON sn.spbh = sc.spbh AND sc.ck = @ck | ... | ... |
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtTjdWorkflowHelper.cs
| ... | ... | @@ -11,6 +11,8 @@ namespace NCC.Extend.WtTjd |
| 11 | 11 | { |
| 12 | 12 | /// <summary> |
| 13 | 13 | /// 商品调价单审批流(待办中心 / ApproveGeneric 转发),非独立 HTTP 控制器。 |
| 14 | + /// 注意:调价单调的是"成本",审核通过后将明细 <c>Thdj</c>(调后单价)写入 <c>wt_sp_cost.cbj</c>, | |
| 15 | + /// 不再写商品档案零售价 <c>wt_sp.F_Lsj</c>。 | |
| 14 | 16 | /// </summary> |
| 15 | 17 | public class WtTjdWorkflowHelper : ITransient |
| 16 | 18 | { |
| ... | ... | @@ -101,7 +103,7 @@ namespace NCC.Extend.WtTjd |
| 101 | 103 | foreach (var mx in mxList) |
| 102 | 104 | { |
| 103 | 105 | if (string.IsNullOrWhiteSpace(mx.Spbh) || mx.Thdj <= 0) |
| 104 | - return new { success = false, message = "明细中存在未填写商品或调后售价为 0,请检查后再提交" }; | |
| 106 | + return new { success = false, message = "明细中存在未填写商品或调后成本为 0,请检查后再提交" }; | |
| 105 | 107 | } |
| 106 | 108 | |
| 107 | 109 | entity.Djzt = "待审核"; |
| ... | ... | @@ -177,7 +179,7 @@ namespace NCC.Extend.WtTjd |
| 177 | 179 | return new { success = true, message = "一级审核通过,等待二级审核" }; |
| 178 | 180 | } |
| 179 | 181 | |
| 180 | - await ApplyFinalRetailPricesAsync(entity, mxList, userId, remark, "审核通过"); | |
| 182 | + await ApplyFinalCostPricesAsync(entity, mxList, userId, remark, "审核通过"); | |
| 181 | 183 | _db.CommitTran(); |
| 182 | 184 | return new { success = true, message = "审核通过" }; |
| 183 | 185 | } |
| ... | ... | @@ -200,7 +202,7 @@ namespace NCC.Extend.WtTjd |
| 200 | 202 | return new { success = false, message = "当前用户不在商品调价单二级审核人员范围内" }; |
| 201 | 203 | } |
| 202 | 204 | |
| 203 | - await ApplyFinalRetailPricesAsync(entity, mxList, userId, remark, "二级通过"); | |
| 205 | + await ApplyFinalCostPricesAsync(entity, mxList, userId, remark, "二级通过"); | |
| 204 | 206 | _db.CommitTran(); |
| 205 | 207 | return new { success = true, message = "二级审核通过,审核完成" }; |
| 206 | 208 | } |
| ... | ... | @@ -215,7 +217,11 @@ namespace NCC.Extend.WtTjd |
| 215 | 217 | } |
| 216 | 218 | } |
| 217 | 219 | |
| 218 | - private async Task ApplyFinalRetailPricesAsync(WtTjdEntity entity, List<WtTjdMxEntity> mxList, string userId, string remark, string spbzTag) | |
| 220 | + /// <summary> | |
| 221 | + /// 终审通过:将明细 <c>Thdj</c>(调后单价)写入 <c>wt_sp_cost.cbj</c>(按本单仓库 + 商品定位), | |
| 222 | + /// 调价单调的是"成本",不再写商品档案零售价 <c>wt_sp.F_Lsj</c> | |
| 223 | + /// </summary> | |
| 224 | + private async Task ApplyFinalCostPricesAsync(WtTjdEntity entity, List<WtTjdMxEntity> mxList, string userId, string remark, string spbzTag) | |
| 219 | 225 | { |
| 220 | 226 | entity.Djzt = "已审核"; |
| 221 | 227 | entity.Shr = userId; |
| ... | ... | @@ -225,9 +231,13 @@ namespace NCC.Extend.WtTjd |
| 225 | 231 | |
| 226 | 232 | foreach (var mx in mxList) |
| 227 | 233 | { |
| 228 | - await _db.Updateable<WtSpEntity>() | |
| 229 | - .SetColumns(it => new WtSpEntity { Lsj = mx.Thdj }) | |
| 230 | - .Where(it => it.Id == mx.Spbh) | |
| 234 | + var ckId = !string.IsNullOrEmpty(mx.Ck) ? mx.Ck : entity.Ck; | |
| 235 | + if (string.IsNullOrEmpty(ckId) || string.IsNullOrEmpty(mx.Spbh)) continue; | |
| 236 | + | |
| 237 | + var thdj = mx.Thdj; | |
| 238 | + await _db.Updateable<WtSpCostEntity>() | |
| 239 | + .SetColumns(it => new WtSpCostEntity { Cbj = thdj }) | |
| 240 | + .Where(it => it.Spbh == mx.Spbh && it.Ck == ckId) | |
| 231 | 241 | .ExecuteCommandAsync(); |
| 232 | 242 | } |
| 233 | 243 | } | ... | ... |
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtXsckdService.cs
| ... | ... | @@ -64,6 +64,7 @@ namespace NCC.Extend.WtXsckd |
| 64 | 64 | private static bool _syPchColumnChecked; |
| 65 | 65 | private static bool _yddhColumnChecked; |
| 66 | 66 | private static bool _dyddhColumnChecked; |
| 67 | + private static bool _fhrColumnChecked; | |
| 67 | 68 | private static bool _spCostCkMigrated; |
| 68 | 69 | private static bool _salesReturnMxCostSnapshotMigrated; |
| 69 | 70 | private static int _salesReturnMxCostSnapshotRunCount; |
| ... | ... | @@ -391,6 +392,32 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') |
| 391 | 392 | } |
| 392 | 393 | |
| 393 | 394 | /// <summary> |
| 395 | + /// 确保发货人列 fhr 存在(抖音发货单提交销售出库时必填;独立于经手人) | |
| 396 | + /// </summary> | |
| 397 | + private void EnsureFhrColumn() | |
| 398 | + { | |
| 399 | + if (_fhrColumnChecked) return; | |
| 400 | + lock (typeof(WtXsckdService)) | |
| 401 | + { | |
| 402 | + if (_fhrColumnChecked) return; | |
| 403 | + try | |
| 404 | + { | |
| 405 | + if (!_db.DbMaintenance.IsAnyTable("wt_xsckd")) { _fhrColumnChecked = true; return; } | |
| 406 | + var columns = _db.DbMaintenance.GetColumnInfosByTableName("wt_xsckd"); | |
| 407 | + var columnNames = columns.Select(c => c.DbColumnName.ToLower()).ToHashSet(); | |
| 408 | + if (!columnNames.Contains("fhr")) | |
| 409 | + { | |
| 410 | + _db.Ado.ExecuteCommand( | |
| 411 | + "ALTER TABLE `wt_xsckd` ADD COLUMN `fhr` varchar(64) NULL COMMENT '发货人(用户Id)'"); | |
| 412 | + Console.WriteLine("✅ 已添加字段: fhr (发货人)"); | |
| 413 | + } | |
| 414 | + } | |
| 415 | + catch (Exception ex) { Console.WriteLine($"EnsureFhrColumn: {ex.Message}"); } | |
| 416 | + _fhrColumnChecked = true; | |
| 417 | + } | |
| 418 | + } | |
| 419 | + | |
| 420 | + /// <summary> | |
| 394 | 421 | /// 确保付款明细列 fkmx 存在(会员余额/积分抵扣等 JSON) |
| 395 | 422 | /// </summary> |
| 396 | 423 | private void EnsureFkmxColumn() |
| ... | ... | @@ -1721,6 +1748,7 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') |
| 1721 | 1748 | EnsureSyPchColumn(); |
| 1722 | 1749 | EnsureYddhColumn(); |
| 1723 | 1750 | EnsureDyddhColumn(); |
| 1751 | + EnsureFhrColumn(); | |
| 1724 | 1752 | MigrateSpCostCkToWarehouseId(); |
| 1725 | 1753 | var entity = await _db.Queryable<WtXsckdEntity>().FirstAsync(p => p.Id == id); |
| 1726 | 1754 | _ = entity ?? throw NCCException.Oh(ErrorCode.COM1005); |
| ... | ... | @@ -1930,8 +1958,9 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') |
| 1930 | 1958 | EnsureSyPchColumn(); |
| 1931 | 1959 | EnsureYddhColumn(); |
| 1932 | 1960 | EnsureDyddhColumn(); |
| 1961 | + EnsureFhrColumn(); | |
| 1933 | 1962 | EnsureBjsxColumn(); |
| 1934 | - var sidx = input.sidx == null ? "id" : input.sidx; | |
| 1963 | + var sidx = string.IsNullOrWhiteSpace(input.sidx) ? "id" : input.sidx; | |
| 1935 | 1964 | var ycddhFilter = string.IsNullOrWhiteSpace(input.ycddh) ? null : input.ycddh.Trim(); |
| 1936 | 1965 | string linkedReturnExcludeDjlx = null; |
| 1937 | 1966 | var excludeConsignmentLinkedReturn = false; |
| ... | ... | @@ -2000,6 +2029,8 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') |
| 2000 | 2029 | rkck = xsckd.Rkck, |
| 2001 | 2030 | // jsr=xsckd.Jsr, |
| 2002 | 2031 | jsr = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == xsckd.Jsr).Select(u => u.RealName), |
| 2032 | + // 发货人:存用户 Id,展示 RealName | |
| 2033 | + fhr = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == xsckd.Fhr).Select(u => u.RealName), | |
| 2003 | 2034 | ydje = xsckd.Ydje, |
| 2004 | 2035 | cbje = xsckd.Cbje, |
| 2005 | 2036 | bjsx = xsckd.Bjsx, |
| ... | ... | @@ -2027,7 +2058,9 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') |
| 2027 | 2058 | ly = xsckd.Ly, |
| 2028 | 2059 | ycddh = xsckd.Ycddh, |
| 2029 | 2060 | ytwtfhd = xsckd.YtWtfhd, |
| 2030 | - sy_pch = xsckd.SyPch | |
| 2061 | + sy_pch = xsckd.SyPch, | |
| 2062 | + yddh = xsckd.Yddh, | |
| 2063 | + dyddh = xsckd.Dyddh | |
| 2031 | 2064 | }).MergeTable() |
| 2032 | 2065 | // ycddh 条件放在 MergeTable 之后:Join+Select 子查询路径上部分 Where 在分页前未正确生效 |
| 2033 | 2066 | .WhereIF(!string.IsNullOrEmpty(ycddhFilter), it => |
| ... | ... | @@ -2182,7 +2215,11 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') |
| 2182 | 2215 | private async Task EnrichListOutputGysWldwDisplayAsync(List<WtXsckdListOutput> items) |
| 2183 | 2216 | { |
| 2184 | 2217 | if (items == null || items.Count == 0) return; |
| 2185 | - var targets = items.Where(x => x.djlx == "采购入库单" || x.djlx == "采购退货单").ToList(); | |
| 2218 | + var targets = items.Where(x => | |
| 2219 | + { | |
| 2220 | + var lx = (x.djlx ?? string.Empty).Trim(); | |
| 2221 | + return lx == "采购入库单" || lx == "采购退货单"; | |
| 2222 | + }).ToList(); | |
| 2186 | 2223 | if (targets.Count == 0) return; |
| 2187 | 2224 | var billIds = targets.Select(x => x.id).Where(id => !string.IsNullOrEmpty(id)).Select(id => id.Trim()).Distinct().ToList(); |
| 2188 | 2225 | if (billIds.Count == 0) return; |
| ... | ... | @@ -2207,6 +2244,24 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') |
| 2207 | 2244 | .Where(x => !string.IsNullOrEmpty(x.Id)) |
| 2208 | 2245 | .ToDictionary(x => x.Id.Trim(), x => x.Dwmc ?? "", StringComparer.Ordinal); |
| 2209 | 2246 | |
| 2247 | + var unresolvedForLegacyGys = rawGysIds | |
| 2248 | + .Where(k => !wldwDwmcById.TryGetValue(k, out var mc) || string.IsNullOrWhiteSpace(mc)) | |
| 2249 | + .Distinct() | |
| 2250 | + .ToList(); | |
| 2251 | + Dictionary<string, string> gysMcById = new Dictionary<string, string>(StringComparer.Ordinal); | |
| 2252 | + if (unresolvedForLegacyGys.Count > 0) | |
| 2253 | + { | |
| 2254 | + var legacyRows = await _db.Queryable<WtGysEntity>() | |
| 2255 | + .Where(g => unresolvedForLegacyGys.Contains(g.Id)) | |
| 2256 | + .Select(g => new { g.Id, g.Gysmc }) | |
| 2257 | + .ToListAsync(); | |
| 2258 | + foreach (var r in legacyRows) | |
| 2259 | + { | |
| 2260 | + if (string.IsNullOrEmpty(r.Id) || string.IsNullOrWhiteSpace(r.Gysmc)) continue; | |
| 2261 | + gysMcById[r.Id.Trim()] = r.Gysmc.Trim(); | |
| 2262 | + } | |
| 2263 | + } | |
| 2264 | + | |
| 2210 | 2265 | foreach (var row in targets) |
| 2211 | 2266 | { |
| 2212 | 2267 | var bid = row.id?.Trim(); |
| ... | ... | @@ -2215,6 +2270,10 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') |
| 2215 | 2270 | var key = rawGys.Trim(); |
| 2216 | 2271 | if (wldwDwmcById.TryGetValue(key, out var dwmc) && !string.IsNullOrWhiteSpace(dwmc)) |
| 2217 | 2272 | row.gys = dwmc.Trim(); |
| 2273 | + else if (gysMcById.TryGetValue(key, out var gysmc) && !string.IsNullOrWhiteSpace(gysmc)) | |
| 2274 | + row.gys = gysmc; | |
| 2275 | + else | |
| 2276 | + row.gys = key; | |
| 2218 | 2277 | } |
| 2219 | 2278 | } |
| 2220 | 2279 | |
| ... | ... | @@ -2341,7 +2400,7 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') |
| 2341 | 2400 | jsrLookup = entity.Zdr; |
| 2342 | 2401 | } |
| 2343 | 2402 | |
| 2344 | - var ids = new[] { jsrLookup, entity.Zdr, entity.Shr, entity.Gzr } | |
| 2403 | + var ids = new[] { jsrLookup, entity.Zdr, entity.Shr, entity.Gzr, entity.Fhr } | |
| 2345 | 2404 | .Where(s => !string.IsNullOrWhiteSpace(s)) |
| 2346 | 2405 | .Select(s => s.Trim()) |
| 2347 | 2406 | .Distinct() |
| ... | ... | @@ -2389,6 +2448,8 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') |
| 2389 | 2448 | await TrySetNameAsync(entity.Zdr, v => output.zdr = v); |
| 2390 | 2449 | await TrySetNameAsync(entity.Shr, v => output.shr = v); |
| 2391 | 2450 | await TrySetNameAsync(entity.Gzr, v => output.gzr = v); |
| 2451 | + // 发货人:抖音单传入的是用户 Id,这里转 RealName;不允许 hy 兜底,避免误把会员名作发货人 | |
| 2452 | + await TrySetNameAsync(entity.Fhr, v => output.fhr = v, allowHyFallback: false); | |
| 2392 | 2453 | } |
| 2393 | 2454 | |
| 2394 | 2455 | /// <summary> |
| ... | ... | @@ -2536,6 +2597,7 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') |
| 2536 | 2597 | EnsureSyPchColumn(); |
| 2537 | 2598 | EnsureYddhColumn(); |
| 2538 | 2599 | EnsureDyddhColumn(); |
| 2600 | + EnsureFhrColumn(); | |
| 2539 | 2601 | EnsureBjsxColumn(); |
| 2540 | 2602 | EnsureBjhcbColumn(); |
| 2541 | 2603 | // 匿名访问时,userInfo可能为null,需要安全处理 |
| ... | ... | @@ -3029,8 +3091,9 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') |
| 3029 | 3091 | EnsureSyPchColumn(); |
| 3030 | 3092 | EnsureYddhColumn(); |
| 3031 | 3093 | EnsureDyddhColumn(); |
| 3094 | + EnsureFhrColumn(); | |
| 3032 | 3095 | EnsureBjsxColumn(); |
| 3033 | - var sidx = input.sidx == null ? "id" : input.sidx; | |
| 3096 | + var sidx = string.IsNullOrWhiteSpace(input.sidx) ? "id" : input.sidx; | |
| 3034 | 3097 | var ycddhFilterNp = string.IsNullOrWhiteSpace(input.ycddh) ? null : input.ycddh.Trim(); |
| 3035 | 3098 | List<string> queryDjrq = input.djrq != null ? input.djrq.Split(',').ToObeject<List<string>>() : null; |
| 3036 | 3099 | DateTime? startDjrq = queryDjrq != null ? Ext.GetDateTime(queryDjrq.First()) : null; |
| ... | ... | @@ -3079,6 +3142,7 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') |
| 3079 | 3142 | cjck = xsckd.Cjck, |
| 3080 | 3143 | rkck = xsckd.Rkck, |
| 3081 | 3144 | jsr = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == xsckd.Jsr).Select(u => u.RealName), |
| 3145 | + fhr = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == xsckd.Fhr).Select(u => u.RealName), | |
| 3082 | 3146 | ydje = xsckd.Ydje, |
| 3083 | 3147 | cbje = xsckd.Cbje, |
| 3084 | 3148 | bjsx = xsckd.Bjsx, |
| ... | ... | @@ -3099,7 +3163,9 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') |
| 3099 | 3163 | ly = xsckd.Ly, |
| 3100 | 3164 | ycddh = xsckd.Ycddh, |
| 3101 | 3165 | ytwtfhd = xsckd.YtWtfhd, |
| 3102 | - sy_pch = xsckd.SyPch | |
| 3166 | + sy_pch = xsckd.SyPch, | |
| 3167 | + yddh = xsckd.Yddh, | |
| 3168 | + dyddh = xsckd.Dyddh | |
| 3103 | 3169 | }).MergeTable() |
| 3104 | 3170 | .WhereIF(!string.IsNullOrEmpty(ycddhFilterNp), it => |
| 3105 | 3171 | it.ycddh == ycddhFilterNp |
| ... | ... | @@ -3304,6 +3370,7 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') |
| 3304 | 3370 | EnsureSyPchColumn(); |
| 3305 | 3371 | EnsureYddhColumn(); |
| 3306 | 3372 | EnsureDyddhColumn(); |
| 3373 | + EnsureFhrColumn(); | |
| 3307 | 3374 | EnsureBjsxColumn(); |
| 3308 | 3375 | EnsureBjhcbColumn(); |
| 3309 | 3376 | var oldHeader = await _db.Queryable<WtXsckdEntity>().FirstAsync(p => p.Id == id); |
| ... | ... | @@ -5284,8 +5351,24 @@ LIMIT {offset}, {pageSize}"; |
| 5284 | 5351 | return new { serialNumbers = new List<object>() }; |
| 5285 | 5352 | |
| 5286 | 5353 | var code = productCode.Trim(); |
| 5354 | + | |
| 5355 | + // 标准化 productCode:允许前端传 F_Id / F_Spbm / F_Dyspid 任一种,统一到序列号表使用的 F_Id | |
| 5356 | + // 序列号表 spbh 字段存的是 wt_sp.F_Id(商品主键) | |
| 5357 | + var productCodeEncode = ""; | |
| 5358 | + var normalizedSpbh = code; | |
| 5359 | + var spRow = await _db.Queryable<WtSpEntity>() | |
| 5360 | + .Where(p => p.Id == code || p.Spbm == code || p.Dyspid == code) | |
| 5361 | + .Select(p => new { p.Id, p.Spbm }) | |
| 5362 | + .FirstAsync(); | |
| 5363 | + if (spRow != null) | |
| 5364 | + { | |
| 5365 | + if (!string.IsNullOrEmpty(spRow.Id)) normalizedSpbh = spRow.Id; | |
| 5366 | + if (!string.IsNullOrEmpty(spRow.Spbm)) productCodeEncode = spRow.Spbm; | |
| 5367 | + } | |
| 5368 | + | |
| 5369 | + // 使用标准化后的 F_Id 查询序列号表;避免 StringComparison 重载在 SqlSugar 下翻译不稳定 | |
| 5287 | 5370 | var baseQuery = _db.Queryable<WtSerialNumberEntity>() |
| 5288 | - .Where(s => s.Spbh.Trim().Equals(code, StringComparison.OrdinalIgnoreCase)); | |
| 5371 | + .Where(s => s.Spbh == normalizedSpbh); | |
| 5289 | 5372 | |
| 5290 | 5373 | // 根据单据类型决定查询状态 |
| 5291 | 5374 | // 退货单:查询已售出的序列号(Status = 1),因为要退货 |
| ... | ... | @@ -5307,22 +5390,14 @@ LIMIT {offset}, {pageSize}"; |
| 5307 | 5390 | var queryWithWarehouse = baseQuery; |
| 5308 | 5391 | if (!string.IsNullOrEmpty(warehouse)) |
| 5309 | 5392 | { |
| 5310 | - queryWithWarehouse = queryWithWarehouse.Where(s => s.InWarehouse.Equals(warehouse.Trim(), StringComparison.OrdinalIgnoreCase)); | |
| 5393 | + var warehouseTrim = warehouse.Trim(); | |
| 5394 | + queryWithWarehouse = queryWithWarehouse.Where(s => s.InWarehouse == warehouseTrim); | |
| 5311 | 5395 | } |
| 5312 | 5396 | if (!string.IsNullOrEmpty(serialNumber)) |
| 5313 | 5397 | { |
| 5314 | 5398 | queryWithWarehouse = queryWithWarehouse.Where(s => s.SerialNumber.Contains(serialNumber)); |
| 5315 | 5399 | } |
| 5316 | 5400 | |
| 5317 | - // 从商品档案获取商品编码(wt_sp.F_Spbm),用于收银台序列号选择界面显示;支持按主键 F_Id 或按 F_Spbm 匹配 | |
| 5318 | - var productCodeEncode = ""; | |
| 5319 | - var spRow = await _db.Queryable<WtSpEntity>() | |
| 5320 | - .Where(p => p.Id == code || p.Spbm == code) | |
| 5321 | - .Select(p => new { p.Spbm }) | |
| 5322 | - .FirstAsync(); | |
| 5323 | - if (spRow != null && !string.IsNullOrEmpty(spRow.Spbm)) | |
| 5324 | - productCodeEncode = spRow.Spbm; | |
| 5325 | - | |
| 5326 | 5401 | // 先按商品+仓库过滤,如果没有结果,再退回到只按商品查询,避免因为仓库编码不一致导致查不到数据 |
| 5327 | 5402 | var list1 = await queryWithWarehouse |
| 5328 | 5403 | .Select(s => new { | ... | ... |