diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/BillingController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/BillingController.java index ee45b899..1a234967 100644 --- a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/BillingController.java +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/BillingController.java @@ -116,6 +116,59 @@ public class BillingController extends BaseController return success(prcOrderBillService.getBill(billId)); } + /** + * Batch update order credit arrears status (ARREARS -> NORMAL) with available-credit check. + */ + @PreAuthorize("@ss.hasPermi('price:billing:arrearsUpdate')") + @Log(title = "Billing credit arrears", businessType = BusinessType.UPDATE) + @PutMapping("/bill/creditArrears") + public AjaxResult updateCreditArrears(@RequestBody java.util.List billIds) + { + if (billIds == null || billIds.isEmpty()) + { + return error("请勾选要更新的订单"); + } + int success = 0; + java.util.List fails = new java.util.ArrayList(); + for (String billId : billIds) + { + try + { + prcOrderBillService.updateCreditArrearsStatus(billId); + success++; + } + catch (RuntimeException e) + { + String msg = e.getMessage(); + if (msg == null || msg.isEmpty()) + { + msg = e.getClass().getSimpleName(); + } + fails.add((billId == null ? "" : billId) + ": " + msg); + } + } + if (fails.isEmpty()) + { + return success("成功更新 " + success + " 条"); + } + StringBuilder sb = new StringBuilder(); + sb.append("成功 ").append(success).append(" 条,失败 ").append(fails.size()).append(" 条"); + int limit = Math.min(fails.size(), 5); + for (int i = 0; i < limit; i++) + { + sb.append(";").append(fails.get(i)); + } + if (fails.size() > limit) + { + sb.append("…"); + } + if (success > 0) + { + return success(sb.toString()); + } + return error(sb.toString()); + } + @PreAuthorize("@ss.hasPermi('price:billing:feeAdd')") @Log(title = "Billing income fee", businessType = BusinessType.INSERT) @PostMapping("/bill/fee") diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcOrderBillService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcOrderBillService.java index 18eb99e4..2668c25f 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcOrderBillService.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcOrderBillService.java @@ -50,5 +50,13 @@ public interface IPrcOrderBillService */ void clearCreditArrearsByAmount(String customerCode, BigDecimal amount); + /** + * 计费列表手动更新欠费状态:仅 ARREARS 可更为 NORMAL。 + * 校验逻辑与计费同步额度一致(客户正常 + 有足够可用额度),不通过则拒绝。 + * + * @param billId 订单计费ID + * @return 更新后的计费结果 + */ + TmsPriceQuoteResult updateCreditArrearsStatus(String billId); } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java index a67119d4..903a10d1 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java @@ -22,6 +22,7 @@ import com.ruoyi.ar.enums.ArIncomeBillFeeStatus; import com.ruoyi.ar.enums.ArOrderBillStatus; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.sign.Md5Utils; import com.ruoyi.price.domain.PrcIncomeBill; @@ -530,6 +531,76 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService } } + + /** + * 计费列表手动更新欠费状态:校验可用额度(与计费同步一致)后将 ARREARS 更为 NORMAL。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public TmsPriceQuoteResult updateCreditArrearsStatus(String billId) + { + if (StringUtils.isEmpty(billId)) + { + throw new ServiceException(MessageUtils.message("price.bill.billIdRequired")); + } + PrcOrderBill bill = orderBillMapper.selectPrcOrderBillById(billId.trim()); + if (bill == null) + { + throw new ServiceException(MessageUtils.message("price.bill.notFound")); + } + if (!BillCreditArrearsStatus.ARREARS.getCode().equals(bill.getCreditArrearsStatus())) + { + throw new ServiceException("仅欠费订单可更新为正常"); + } + String customerActive = normalizeCustomerActiveStatus(bill.getCustomerActiveStatus()); + if (!BillCustomerActiveStatus.ACTIVE.getCode().equals(customerActive)) + { + throw new ServiceException("客户状态非正常,不能更新欠费状态"); + } + // 与计费同步一致:已欠费或可用额度不足则拒绝(本单额度已占用,按可用额度>=0 判定) + if (isCreditInsufficient(bill.getCustomerId(), bill.getCustomerCode(), BigDecimal.ZERO)) + { + throw new ServiceException("可用额度不足,不能更新为正常"); + } + String operator = SecurityUtils.getUsername(); + if (StringUtils.isEmpty(operator)) + { + operator = "system"; + } + String normal = BillCreditArrearsStatus.NORMAL.getCode(); + orderBillMapper.updateCreditArrearsStatus(bill.getBillId(), normal, operator); + bill.setCreditArrearsStatus(normal); + saveManualArrearsUpdateEsLog(bill, true, null); + return toResult(bill, incomeBillFeeMapper.selectByBillId(bill.getBillId()), + payBillFeeMapper.selectByBillId(bill.getBillId())); + } + + private void saveManualArrearsUpdateEsLog(PrcOrderBill bill, boolean success, String errorMessage) + { + if (billSearchStore == null || bill == null || StringUtils.isEmpty(bill.getOrderNo())) + { + return; + } + JSONObject req = new JSONObject(); + req.put("action", "MANUAL_CLEAR_ARREARS"); + req.put("scope", "ORDER"); + req.put("orderNo", bill.getOrderNo()); + req.put("billId", bill.getBillId()); + req.put("customerCode", bill.getCustomerCode()); + req.put("incomeTotal", bill.getIncomeTotal()); + req.put("fromStatus", BillCreditArrearsStatus.ARREARS.getCode()); + req.put("toStatus", BillCreditArrearsStatus.NORMAL.getCode()); + JSONObject resp = new JSONObject(); + resp.put("success", Boolean.valueOf(success)); + if (StringUtils.isNotEmpty(errorMessage)) + { + resp.put("msg", errorMessage); + } + String status = success ? EsBizLogSupport.STATUS_SUCCESS : EsBizLogSupport.STATUS_FAIL; + String err = success ? null : EsBizLogSupport.cut(errorMessage); + billSearchStore.save(EsLogType.ARREARS, bill.getOrderNo(), req.toJSONString(), resp.toJSONString(), status, err); + } + private TmsOrderBillMatchType resolveTmsMatchType(String identifier, PrcOrderBill bill) { if (identifier.equalsIgnoreCase(StringUtils.trim(bill.getOrderNo()))) diff --git a/bms-back/sql/price/billing_arrears_menu.sql b/bms-back/sql/price/billing_arrears_menu.sql new file mode 100644 index 00000000..b85dc5b5 --- /dev/null +++ b/bms-back/sql/price/billing_arrears_menu.sql @@ -0,0 +1,14 @@ +-- Billing list: update credit arrears status permission (parent = price:billing:bill) +SET NAMES utf8mb4; + +SET @parent_id := (SELECT menu_id FROM sys_menu WHERE perms = 'price:billing:bill' AND menu_type = 'C' LIMIT 1); + +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT N'更新欠费状态', @parent_id, 13, '', NULL, 1, 0, 'F', '0', '0', 'price:billing:arrearsUpdate', '#', 'admin', sysdate(), N'计费列表手动更新订单额度欠费状态(需校验可用额度)' +FROM DUAL +WHERE @parent_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'price:billing:arrearsUpdate'); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, menu_id FROM sys_menu WHERE perms = 'price:billing:arrearsUpdate' + AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = sys_menu.menu_id); diff --git a/bms-front/src/api/price/billing.js b/bms-front/src/api/price/billing.js index 522e3e67..6bfe81e4 100644 --- a/bms-front/src/api/price/billing.js +++ b/bms-front/src/api/price/billing.js @@ -104,3 +104,12 @@ export function deleteIncomeBillFee(feeId) { method: 'delete' }) } + +/** 计费列表:批量更新订单额度欠费状态(校验可用额度) */ +export function updateCreditArrears(billIds) { + return request({ + url: '/price/billing/bill/creditArrears', + method: 'put', + data: billIds + }) +} diff --git a/bms-front/src/components/BasicForm/FormControl.vue b/bms-front/src/components/BasicForm/FormControl.vue index ba4a5c57..e46012e0 100644 --- a/bms-front/src/components/BasicForm/FormControl.vue +++ b/bms-front/src/components/BasicForm/FormControl.vue @@ -159,7 +159,9 @@ const props = defineProps({ // schema 项配置 item: { type: Object, required: true }, // 字典映射表(来自父级 BasicForm) - dictMap: { type: Object, default: () => ({}) } + dictMap: { type: Object, default: () => ({}) }, + // 拉伸模式(固定每行 N 项的栅格布局):忽略 item.width,控件填满单元格 + stretch: { type: Boolean, default: false } }) const comp = computed(() => props.item.component || 'input') @@ -167,8 +169,11 @@ const placeholder = computed(() => props.item.placeholder) const valueKey = computed(() => props.item.valueKey || 'value') const labelKey = computed(() => props.item.labelKey || 'label') -// 控件宽度样式 -const controlStyle = computed(() => (props.item.width ? { width: props.item.width } : null)) +// 控件宽度样式(stretch 模式下填满单元格) +const controlStyle = computed(() => { + if (props.stretch) return { width: '100%' } + return props.item.width ? { width: props.item.width } : null +}) // 选项:静态 options 优先,其次字典 const options = computed(() => { diff --git a/bms-front/src/components/BasicForm/index.vue b/bms-front/src/components/BasicForm/index.vue index 4910e349..d5ab3a38 100644 --- a/bms-front/src/components/BasicForm/index.vue +++ b/bms-front/src/components/BasicForm/index.vue @@ -8,7 +8,7 @@ :label-position="labelPosition" :disabled="disabled" :size="size" - :class="['basic-form', { 'basic-form--grid': !inline }]" + :class="['basic-form', { 'basic-form--grid': !inline, 'basic-form--perrow': inline && perRow > 0 }]" > @@ -81,6 +82,7 @@ * valueKey: 'value', // 选项值字段(默认 value) * labelKey: 'label', // 选项标签字段(默认 label) * span: 12, // 栅格占位(grid 布局,默认 24) + * colSpan: 2, // 占据的列数(行内 perRow 栅格模式,默认 1) * width: '200px', // 控件宽度 * show: true, // 是否显示 * disabled: false, @@ -117,6 +119,11 @@ const props = defineProps({ type: Boolean, default: false }, + // 行内布局下固定每行项数(>0 时启用栅格模式,控件拉伸填满单元格;0 表示自动流式换行) + perRow: { + type: Number, + default: 0 + }, // 标签宽度 labelWidth: { type: [String, Number], @@ -239,6 +246,26 @@ defineExpose({ margin-bottom: 18px; } +/* 行内固定每行 N 项:CSS 栅格,多余项自动换行 */ +.basic-form--perrow { + display: grid; + grid-template-columns: repeat(v-bind(perRow), minmax(0, 1fr)); + column-gap: 16px; + row-gap: 2px; +} +.basic-form--perrow :deep(.el-form-item) { + display: flex; + margin-right: 0; +} +.basic-form--perrow :deep(.el-form-item__content) { + flex: 1; + min-width: 0; +} +/* 操作区居中时独占整行 */ +.basic-form--perrow .basic-form-action--center { + grid-column: 1 / -1; +} + /* 操作区居中:占满整行、内容水平居中(搜索按钮换行后居中显示的场景) */ .basic-form .basic-form-action--center { width: 100%; diff --git a/bms-front/src/components/BasicTable/index.vue b/bms-front/src/components/BasicTable/index.vue index 8085c22d..d2786b4e 100644 --- a/bms-front/src/components/BasicTable/index.vue +++ b/bms-front/src/components/BasicTable/index.vue @@ -9,6 +9,7 @@ :rules="searchRules" :label-width="searchLabelWidth" :action-align="searchActionAlign" + :per-row="searchPerRow" :size="size" inline v-show="showSearch" @@ -184,6 +185,11 @@ const props = defineProps({ type: String, default: 'left' }, + // 搜索区固定每行项数(>0 时启用栅格布局,多余项自动换行;0 为流式换行) + searchPerRow: { + type: Number, + default: 0 + }, // 列表加载函数:(params) => Promise<{rows, total}> | {rows, total} query: { type: Function, diff --git a/bms-front/src/views/price/billing/bill.vue b/bms-front/src/views/price/billing/bill.vue index 33c61356..bb0a280e 100644 --- a/bms-front/src/views/price/billing/bill.vue +++ b/bms-front/src/views/price/billing/bill.vue @@ -6,6 +6,7 @@ :search-columns="searchColumns" :query="queryList" search-action-align="center" + :search-per-row="4" row-key="billId" >