This commit is contained in:
zlwu committed 2026-07-06 19:41:24 +08:00
1 parent cdaa0ac273
commit b7cd657f32
32 files changed
+2885 -1936

No files matched your search

+61 -37
View File
@@ -1,5 +1,5 @@
/**
* 采集协议单 · 缴费模式切换(dj ↔ fq)
* 采集协议单 · 缴费模式切换(dj / fq / djfq)
* 规则见 docs/superpowers/specs/2026-06-04-protocol-pay-mode-switch-design.md
*/
@@ -8,8 +8,9 @@ import {
DEFAULT_INSTALLMENT_ADVANCE_DAYS,
defaultInstallmentAmounts
} from '@/utils/protocol-payment-installment'
import { type PayMode, isInstallmentPayMode } from '@/utils/protocol-payment-presets'
export type PayMode = 'dj' | 'fq'
export type { PayMode }
export type DetailLike = Record<string, any>
const LOCKED_STATUSES = new Set(['jfqrz', 'yjf', 'tfqrz', 'ytf'])
@@ -69,6 +70,35 @@ function countByStatus(details: DetailLike[], status: string): number {
return details.filter((d) => String(d.payStatus || 'dfj') === status).length
}
function buildInstallmentDetails(
ctx: PayModeSwitchContext,
rows: DetailLike[]
): DetailLike[] {
const count = Number(ctx.installmentCount) || 6
const interval = Number(ctx.installmentIntervalMonths) || 6
const startDate = ctx.installmentStartDate || ''
const advanceDays = Number(ctx.installmentAdvanceDays ?? DEFAULT_INSTALLMENT_ADVANCE_DAYS)
const amounts =
ctx.installmentRatios?.length && sumPaidAmount(rows) === 0
? ctx.installmentRatios.map((v) => Number(v || 0))
: defaultInstallmentAmounts(ctx.contractAmount, count)
const result: DetailLike[] = []
for (let i = 0; i < count; i++) {
result.push(
createPaymentDetailRow({
dueAmount: amounts[i] ?? null,
installmentNo: i + 1,
installmentDueDate:
startDate && interval
? calcFqInstallmentDueDate(startDate, i, interval, advanceDays)
: '',
expanded: i === 0
})
)
}
return result
}
export function createPaymentDetailRow(options: {
dueAmount: number | null
installmentDueDate?: string
@@ -110,7 +140,7 @@ export function evaluatePayModeSwitch(
}
}
if (fromMode === 'dj' && toMode === 'fq') {
if (fromMode === 'dj' && isInstallmentPayMode(toMode)) {
if (rows.some((d) => isPayDetailLocked(d))) {
return {
action: 'blocked',
@@ -118,13 +148,14 @@ export function evaluatePayModeSwitch(
}
}
const count = ctx.installmentCount || 6
const label = toMode === 'djfq' ? '趸交+分期' : '分期'
return {
action: 'confirm',
summary: `将清除当前趸交计划,按 ${count} 期分期重新生成缴费明细。是否继续?`
summary: `将清除当前趸交计划,按 ${count} 期${label}重新生成缴费明细。是否继续?`
}
}
if (fromMode === 'fq' && toMode === 'dj') {
if (isInstallmentPayMode(fromMode) && toMode === 'dj') {
const dfjCount = countByStatus(rows, 'dfj')
const yjfCount = countByStatus(rows, 'yjf')
@@ -156,6 +187,21 @@ export function evaluatePayModeSwitch(
}
}
if (isInstallmentPayMode(fromMode) && isInstallmentPayMode(toMode) && fromMode !== toMode) {
if (rows.some((d) => isPayDetailLocked(d))) {
return {
action: 'blocked',
reason: '分期已有缴费记录,无法切换分期类型。'
}
}
const count = ctx.installmentCount || 6
const label = toMode === 'djfq' ? '趸交+分期' : '分期'
return {
action: 'confirm',
summary: `将按 ${count} 期${label}重新生成缴费明细。是否继续?`
}
}
return { action: 'noop' }
}
@@ -167,33 +213,11 @@ export function applyPayModeSwitch(
): DetailLike[] {
const rows = activeDetails(details)
if (fromMode === 'dj' && toMode === 'fq') {
const count = Number(ctx.installmentCount) || 6
const interval = Number(ctx.installmentIntervalMonths) || 6
const startDate = ctx.installmentStartDate || ''
const advanceDays = Number(ctx.installmentAdvanceDays ?? DEFAULT_INSTALLMENT_ADVANCE_DAYS)
const amounts =
ctx.installmentRatios?.length && sumPaidAmount(rows) === 0
? ctx.installmentRatios.map((v) => Number(v || 0))
: defaultInstallmentAmounts(ctx.contractAmount, count)
const result: DetailLike[] = []
for (let i = 0; i < count; i++) {
result.push(
createPaymentDetailRow({
dueAmount: amounts[i] ?? null,
installmentNo: i + 1,
installmentDueDate:
startDate && interval
? calcFqInstallmentDueDate(startDate, i, interval, advanceDays)
: '',
expanded: i === 0
})
)
}
return result
if (fromMode === 'dj' && isInstallmentPayMode(toMode)) {
return buildInstallmentDetails(ctx, rows)
}
if (fromMode === 'fq' && toMode === 'dj') {
if (isInstallmentPayMode(fromMode) && toMode === 'dj') {
const yjfRows = rows
.filter((d) => String(d.payStatus) === 'yjf')
.map((d) => ({ ...d, _payModeLocked: true }))
@@ -209,14 +233,14 @@ export function applyPayModeSwitch(
return [...yjfRows, newDjRow]
}
if (isInstallmentPayMode(fromMode) && isInstallmentPayMode(toMode) && fromMode !== toMode) {
return buildInstallmentDetails(ctx, rows)
}
return rows
}
/** 趸交模式下是否还可添加待缴明细 */
export function canAddPaymentDetail(payMode: string, details: DetailLike[]): boolean {
if (payMode === 'fq') return true
const dfjCount = details.filter(
(d) => !isPayDetailLocked(d) && String(d.payStatus || 'dfj') === 'dfj'
).length
return dfjCount < 1
/** 趸交与分期均允许多条缴费明细(合计须等于合同金额) */
export function canAddPaymentDetail(_payMode: string, _details: DetailLike[]): boolean {
return true
}
@@ -1,5 +1,7 @@
/** 分期缴费:期数对齐、应缴日/金额计算(与 ProtocolFormDialog 逻辑一致) */
import { isInstallmentPayMode } from '@/utils/protocol-payment-presets'
const DAYS_PER_MONTH = 30
export const DEFAULT_INSTALLMENT_ADVANCE_DAYS = 30
@@ -311,7 +313,7 @@ export function alignFqPaymentDetailsForDisplay(
payment: PaymentLike,
details: DetailLike[]
): DetailLike[] {
if (!payment || payment.payMode !== 'fq') return details
if (!payment || !isInstallmentPayMode(String(payment.payMode || ''))) return details
const count = Number(payment.installmentCount)
if (!count || count < 1) return details
@@ -396,7 +398,7 @@ export function syncFqPaymentDetailRowCount(
},
paymentDetails: DetailLike[]
): void {
if (payment.payMode !== 'fq') return
if (!isInstallmentPayMode(payment.payMode)) return
const count = Number(payment.installmentCount)
if (!count || count < 1) return
@@ -0,0 +1,58 @@
/** 采集协议单 · 缴费默认定价表(样本数 × 缴费模式) */
export type PayMode = 'dj' | 'fq' | 'djfq'
export type PaymentPreset = {
contractAmount: number
installmentCount?: number
installmentAmounts?: number[]
}
const PRESETS: Record<1 | 2, Partial<Record<PayMode, PaymentPreset>>> = {
1: {
dj: { contractAmount: 22880 },
fq: {
contractAmount: 25880,
installmentCount: 6,
installmentAmounts: [6480, 3880, 3880, 3880, 3880, 3880]
}
},
2: {
dj: { contractAmount: 37880 },
djfq: {
contractAmount: 41000,
installmentCount: 4,
installmentAmounts: [29360, 3880, 3880, 3880]
},
fq: {
contractAmount: 44000,
installmentCount: 5,
installmentAmounts: [12960, 7760, 7760, 7760, 7760]
}
}
}
export function getActiveSampleCount(samples: Array<{ sampleStatus?: string }>): 1 | 2 {
const n = samples.filter((s) => String(s.sampleStatus || '') !== 'yzf').length
return (n >= 2 ? 2 : 1) as 1 | 2
}
export function resolvePaymentPreset(
sampleCount: 1 | 2,
payMode: PayMode
): PaymentPreset | null {
return PRESETS[sampleCount]?.[payMode] ?? null
}
export function isInstallmentPayMode(payMode: string): boolean {
return payMode === 'fq' || payMode === 'djfq'
}
export function availablePayModes(sampleCount: 1 | 2): PayMode[] {
return sampleCount === 2 ? ['dj', 'djfq', 'fq'] : ['dj', 'fq']
}
/** 校验缴费模式与样本数是否匹配(如 djfq 仅双样本) */
export function isPayModeValidForSampleCount(payMode: PayMode, sampleCount: 1 | 2): boolean {
return availablePayModes(sampleCount).includes(payMode)
}
@@ -40,7 +40,7 @@ export type ProtocolStandardImportParsed = {
medicalStaff?: string
hospitalName?: string
salesUserName?: string
payMode: 'dj' | 'fq'
payMode: 'dj' | 'fq' | 'djfq'
contractAmount?: number | null
transferTypeLabel?: string
paymentTransfers: ProtocolPaymentTransferHint[]
@@ -193,12 +193,13 @@ function earliestCollectTime(hints: ProtocolSampleImportHint[]): string | undefi
return times.sort((a, b) => a.localeCompare(b))[0]
}
/** 缴费方式:仅「分期」为 fq,其余(含微信/趸交)均为 dj */
/** 缴费方式:趸交+分期 → djfq;仅「分期」→ fq;其余 → dj */
export function parsePayModeFromText(value: string): {
payMode: 'dj' | 'fq'
payMode: 'dj' | 'fq' | 'djfq' | 'djfq'
transferLabel?: string
} {
const v = (value || '').trim()
if (/趸交\s*[++]\s*分期|趸交.*分期/.test(v)) return { payMode: 'djfq' }
if (/分期/.test(v)) return { payMode: 'fq' }
if (/微信|支付宝|银行卡|银行|现金/.test(v)) {
return { payMode: 'dj', transferLabel: v }
+2 -1
View File
@@ -117,7 +117,8 @@ export const auditStatusTagType = createTagTypeGetter(auditStatus)
export const payMode: StatusRegistry = {
dj: { label: '趸交', tagType: 'primary' },
fq: { label: '分期', tagType: 'warning' }
fq: { label: '分期', tagType: 'warning' },
djfq: { label: '趸交+分期', tagType: 'warning' }
}
export const payModeLabel = createLabelGetter(payMode)
export const payModeTagType = createTagTypeGetter(payMode)