feat: 套餐分配生效条件选择
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 9m23s

This commit is contained in:
luo
2026-07-22 14:21:15 +08:00
parent fbfdd01eec
commit 68aa03b538
11 changed files with 500 additions and 90 deletions

View File

@@ -18,6 +18,7 @@ export { CarrierService } from './carrier'
export { PackageSeriesService } from './packageSeries'
export { PackageManageService } from './packageManage'
export { ShopSeriesGrantService } from './shopSeriesGrant'
export { ShopPackageAllocationService } from './shopPackageAllocation'
export { OrderService } from './order'
export { AssetService } from './asset'
export { AgentRechargeService } from './agentRecharge'

View File

@@ -0,0 +1,40 @@
/**
* 店铺套餐分配 API 服务
*/
import { BaseService } from '../BaseService'
import type {
BaseResponse,
CreateShopPackageAllocationRequest,
ShopPackageAllocationResponse,
UpdateShopPackageAllocationExpiryBaseRequest
} from '@/types/api'
export class ShopPackageAllocationService extends BaseService {
/**
* 创建店铺套餐分配
* POST /api/admin/shop-package-allocations
*/
static createShopPackageAllocation(
data: CreateShopPackageAllocationRequest
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
return this.post<BaseResponse<ShopPackageAllocationResponse>>(
'/api/admin/shop-package-allocations',
data
)
}
/**
* 更新店铺套餐分配生效条件
* PATCH /api/admin/shop-package-allocations/{id}/expiry-base
*/
static updateShopPackageAllocationExpiryBase(
id: number,
data: UpdateShopPackageAllocationExpiryBaseRequest
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
return this.patch<BaseResponse<ShopPackageAllocationResponse>>(
`/api/admin/shop-package-allocations/${id}/expiry-base`,
data
)
}
}

View File

@@ -101,6 +101,40 @@ export interface CommissionTierInfo {
*/
export type ExpiryBase = 'from_activation' | 'from_purchase'
// 套餐分配生效条件覆盖值
export type PackageAllocationExpiryBaseOverride = 'from_purchase' | 'from_realname'
// 店铺套餐分配生效条件字段
export interface PackageAllocationExpiryBaseFields {
default_expiry_base?: string | null
default_expiry_base_name?: string | null
expiry_base_override?: PackageAllocationExpiryBaseOverride | null
expiry_base_override_name?: string | null
effective_expiry_base?: string | null
effective_expiry_base_name?: string | null
}
// 创建店铺套餐分配请求
export interface CreateShopPackageAllocationRequest {
shop_id: number
package_id: number
cost_price: number
expiry_base_override: PackageAllocationExpiryBaseOverride | null
}
// 更新店铺套餐分配生效条件请求
export interface UpdateShopPackageAllocationExpiryBaseRequest {
expiry_base_override: PackageAllocationExpiryBaseOverride | null
}
// 店铺套餐分配响应
export interface ShopPackageAllocationResponse extends PackageAllocationExpiryBaseFields {
id: number
shop_id: number
package_id: number
cost_price: number
}
/**
* 套餐响应
*/
@@ -234,7 +268,8 @@ export interface CommissionTier {
/**
* 套餐信息(用于系列授权)
*/
export interface GrantPackageInfo {
export interface GrantPackageInfo extends PackageAllocationExpiryBaseFields {
allocation_id?: number
package_id: number
package_name?: string
package_code?: string

View File

@@ -8,6 +8,7 @@ export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const ElButton: (typeof import('element-plus/es'))['ElButton']
const ElMessage: typeof import('element-plus/es')['ElMessage']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']

View File

@@ -88,17 +88,8 @@
</template>
</ElTableColumn>
<ElTableColumn label="统计范围" width="120">
<template #default="{ row }">
<ElTag size="small" type="warning">
仅自己
<!--{{-->
<!-- row.stat_scope === 'self'-->
<!-- ? '仅自己'-->
<!-- : row.stat_scope === 'self_and_sub'-->
<!-- ? '自己+下级'-->
<!-- : '-'-->
<!--}}-->
</ElTag>
<template #default>
<ElTag size="small" type="warning">仅自己</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="佣金金额" min-width="100">
@@ -160,6 +151,21 @@
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="套餐默认生效条件">
<template #default="{ row }">
{{ row.default_expiry_base_name || '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="覆盖生效条件">
<template #default="{ row }">
{{ row.expiry_base_override_name || '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="最终生效条件">
<template #default="{ row }">
{{ row.effective_expiry_base_name || '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="上架状态" align="center">
<template #default="{ row }">
<ElTag v-if="row.shelf_status === 1" type="success" size="small">上架</ElTag>
@@ -268,6 +274,25 @@
}}
</div>
</ElFormItem>
<ElFormItem label="生效条件">
<ElRadioGroup v-model="packageForm.expiry_base_override">
<ElRadio value="default">跟随套餐默认</ElRadio>
<ElRadio value="from_purchase">购买即生效</ElRadio>
<ElRadio value="from_realname">实名即生效</ElRadio>
</ElRadioGroup>
<div class="form-tip">仅影响后续新订单不影响已购买套餐</div>
</ElFormItem>
<template v-if="packageDialogType === 'edit'">
<ElFormItem label="套餐默认生效条件">
{{ packageForm.default_expiry_base_name || '-' }}
</ElFormItem>
<ElFormItem label="覆盖生效条件">
{{ packageForm.expiry_base_override_name || '-' }}
</ElFormItem>
<ElFormItem label="最终生效条件">
{{ packageForm.effective_expiry_base_name || '-' }}
</ElFormItem>
</template>
</ElForm>
<template #footer>
@@ -304,16 +329,24 @@
ElInputNumber,
ElForm,
ElFormItem,
ElRadioGroup,
ElRadio,
FormInstance,
FormRules
} from 'element-plus'
import { ArrowLeft, Loading, Setting, List, Plus } from '@element-plus/icons-vue'
import { ShopSeriesGrantService, PackageManageService, PackageSeriesService } from '@/api/modules'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import {
ShopSeriesGrantService,
ShopPackageAllocationService,
PackageManageService,
PackageSeriesService
} from '@/api/modules'
import type {
ShopSeriesGrantResponse,
PackageResponse,
GrantPackageItem,
GrantPackageInfo,
PackageAllocationExpiryBaseOverride,
PackageSeriesResponse
} from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
@@ -334,6 +367,8 @@
const availablePackages = ref<PackageResponse[]>([])
const packageFormRef = ref<FormInstance>()
type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
// 套餐表单
const packageForm = ref<{
package_id?: number
@@ -341,8 +376,16 @@
package_code?: string
original_cost_price?: number
cost_price_yuan: number
allocation_id?: number
expiry_base_override: ExpiryBaseSelection
initial_expiry_base_override: ExpiryBaseSelection
default_expiry_base_name?: string | null
expiry_base_override_name?: string | null
effective_expiry_base_name?: string | null
}>({
cost_price_yuan: 0
cost_price_yuan: 0,
expiry_base_override: 'default',
initial_expiry_base_override: 'default'
})
// 表单验证规则
@@ -463,7 +506,9 @@
package_name: undefined,
package_code: undefined,
original_cost_price: undefined,
cost_price_yuan: 0
cost_price_yuan: 0,
expiry_base_override: 'default',
initial_expiry_base_override: 'default'
}
// 加载可用套餐
@@ -482,7 +527,13 @@
package_name: row.package_name,
package_code: row.package_code,
original_cost_price: row.cost_price / 100,
cost_price_yuan: row.cost_price / 100
cost_price_yuan: row.cost_price / 100,
allocation_id: row.allocation_id,
expiry_base_override: row.expiry_base_override ?? 'default',
initial_expiry_base_override: row.expiry_base_override ?? 'default',
default_expiry_base_name: row.default_expiry_base_name,
expiry_base_override_name: row.expiry_base_override_name,
effective_expiry_base_name: row.effective_expiry_base_name
}
packageDialogVisible.value = true
@@ -539,23 +590,48 @@
submitLoading.value = true
try {
const packages: GrantPackageItem[] = []
if (packageDialogType.value === 'add') {
// 添加模式:添加新套餐
packages.push({
package_id: packageForm.value.package_id,
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
await ShopPackageAllocationService.createShopPackageAllocation({
shop_id: detailData.value!.shop_id,
package_id: packageForm.value.package_id!,
cost_price: Math.round(packageForm.value.cost_price_yuan * 100),
expiry_base_override:
packageForm.value.expiry_base_override === 'default'
? null
: packageForm.value.expiry_base_override
})
} else {
// 编辑模式:更新套餐成本价
packages.push({
package_id: packageForm.value.package_id,
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
})
}
const expiryBaseChanged =
packageForm.value.expiry_base_override !==
packageForm.value.initial_expiry_base_override
if (expiryBaseChanged && !packageForm.value.allocation_id) {
ElMessage.error('该套餐分配记录缺少ID无法更新生效条件')
return
}
await ShopSeriesGrantService.manageGrantPackages(grantId, { packages })
const packages: GrantPackageItem[] = [
{
package_id: packageForm.value.package_id,
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
}
]
await Promise.all([
ShopSeriesGrantService.manageGrantPackages(grantId, { packages }),
...(expiryBaseChanged
? [
ShopPackageAllocationService.updateShopPackageAllocationExpiryBase(
packageForm.value.allocation_id!,
{
expiry_base_override:
packageForm.value.expiry_base_override === 'default'
? null
: packageForm.value.expiry_base_override
}
)
]
: [])
])
}
ElMessage.success(packageDialogType.value === 'add' ? '添加成功' : '更新成功')
packageDialogVisible.value = false
@@ -613,7 +689,9 @@
package_name: undefined,
package_code: undefined,
original_cost_price: undefined,
cost_price_yuan: 0
cost_price_yuan: 0,
expiry_base_override: 'default',
initial_expiry_base_override: 'default'
}
availablePackages.value = []
}

View File

@@ -170,6 +170,13 @@
{{ formatSuggestedRetailPrice(pkg.suggested_retail_price) }})
</span>
</div>
<div class="expiry-base-input-wrapper">
<ElRadioGroup v-model="pkg.expiry_base_override">
<ElRadio value="default">跟随套餐默认</ElRadio>
<ElRadio value="from_purchase">购买即生效</ElRadio>
<ElRadio value="from_realname">实名即生效</ElRadio>
</ElRadioGroup>
</div>
<ElButton type="danger" size="small" @click="removePackage(index)"
>删除</ElButton
>
@@ -178,6 +185,7 @@
<div class="form-tip"
>设置每个套餐的成本价单位不能低于成本价不能高于建议售价的50%溢价</div
>
<div class="form-tip">生效条件仅影响后续新订单不影响已购买套餐</div>
</ElFormItem>
</template>
</div>
@@ -253,7 +261,7 @@
</template>
</ElTableColumn>
<ElTableColumn label="统计范围" width="140">
<template #default="{ row }">
<template #default>
<ElTag size="small" type="warning"> 仅自己 </ElTag>
</template>
</ElTableColumn>
@@ -427,7 +435,7 @@
</template>
</ElTableColumn>
<ElTableColumn label="统计范围" width="140">
<template #default="{ row }">
<template #default>
<ElTag size="small" type="warning"> 仅自己 </ElTag>
</template>
</ElTableColumn>
@@ -546,6 +554,7 @@
import { useRouter } from 'vue-router'
import {
ShopSeriesGrantService,
ShopPackageAllocationService,
PackageSeriesService,
ShopService,
PackageManageService
@@ -558,8 +567,7 @@
ElRow,
ElCol,
ElSteps,
ElStep,
ElEmpty
ElStep
} from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type {
@@ -567,7 +575,8 @@
PackageSeriesResponse,
ShopResponse,
PackageResponse,
CommissionTier
CommissionTier,
PackageAllocationExpiryBaseOverride
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
@@ -604,8 +613,6 @@
const currentStep = ref(0) // 当前步骤0 为第一步1 为第二步
const seriesOptions = ref<PackageSeriesResponse[]>([])
const shopOptions = ref<ShopResponse[]>([])
const shopTreeData = ref<ShopResponse[]>([])
const packageOptions = ref<PackageResponse[]>([])
const selectedPackageIds = ref<number[]>([])
const searchSeriesOptions = ref<PackageSeriesResponse[]>([])
@@ -777,6 +784,7 @@
cost_price: number
original_cost_price?: number
suggested_retail_price?: number
expiry_base_override: PackageAllocationExpiryBaseOverride | 'default'
}> // 套餐配置
})
@@ -1027,39 +1035,6 @@
return actions
}
// 构建树形结构数据
const buildTreeData = (items: ShopResponse[]) => {
const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>()
const tree: ShopResponse[] = []
// 先将所有项放入 map
items.forEach((item) => {
map.set(item.id, { ...item, children: [] })
})
// 构建树形结构
items.forEach((item) => {
const node = map.get(item.id)!
if (item.parent_id && map.has(item.parent_id)) {
// 有父节点,添加到父节点的 children 中
const parent = map.get(item.parent_id)!
if (!parent.children) parent.children = []
parent.children.push(node)
} else {
// 没有父节点或父节点不存在,作为根节点
tree.push(node)
}
})
return tree
}
// 店铺树节点过滤方法
const filterShopNode = (value: string, data: any) => {
if (!value) return true
return data.shop_name.toLowerCase().includes(value.toLowerCase())
}
onMounted(() => {
getTableData()
})
@@ -1170,7 +1145,8 @@
suggested_retail_price:
pkg?.suggested_retail_price !== undefined && pkg?.suggested_retail_price !== null
? pkg.suggested_retail_price / 100
: undefined
: undefined,
expiry_base_override: 'default'
})
}
})
@@ -1597,7 +1573,8 @@
pkgOption?.suggested_retail_price !== undefined &&
pkgOption?.suggested_retail_price !== null
? pkgOption.suggested_retail_price / 100
: undefined
: undefined,
expiry_base_override: pkg.expiry_base_override ?? 'default'
}
})
selectedPackageIds.value = detail.packages.map((pkg) => pkg.package_id)
@@ -1745,8 +1722,15 @@
data.force_recharge_amount = 0
}
// 可选:套餐配置(将元转换为分)
if (form.packages.length > 0) {
// 新增系列授权后,套餐通过独立分配接口创建,确保显式传递生效条件覆盖值。
const packageAllocations = form.packages.map((pkg: any) => ({
package_id: pkg.package_id,
cost_price: Math.round((pkg.cost_price ?? 0) * 100),
expiry_base_override:
pkg.expiry_base_override === 'default' ? null : pkg.expiry_base_override
}))
if (packageAllocations.length > 0) {
// 验证是否包含赠送套餐
for (const pkg of form.packages) {
const pkgInfo = packageOptions.value.find((p) => p.id === pkg.package_id)
@@ -1778,11 +1762,6 @@
return
}
}
data.packages = form.packages.map((pkg: any) => ({
package_id: pkg.package_id,
cost_price: Math.round((pkg.cost_price ?? 0) * 100) // 将元转换为分
}))
}
if (dialogType.value === 'add') {
@@ -1790,6 +1769,19 @@
data.series_id = form.series_id
data.shop_id = form.shop_id
await ShopSeriesGrantService.createShopSeriesGrant(data)
await Promise.all(
packageAllocations.map(
(pkg: {
package_id: number
cost_price: number
expiry_base_override: PackageAllocationExpiryBaseOverride | null
}) =>
ShopPackageAllocationService.createShopPackageAllocation({
shop_id: data.shop_id,
...pkg
})
)
)
ElMessage.success('新增成功')
} else {
// 编辑时调用更新接口

View File

@@ -40,6 +40,21 @@
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="套餐默认生效条件" minWidth="150">
<template #default="{ row }">
{{ row.default_expiry_base_name || '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="覆盖生效条件" minWidth="150">
<template #default="{ row }">
{{ row.expiry_base_override_name || '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="最终生效条件" minWidth="150">
<template #default="{ row }">
{{ row.effective_expiry_base_name || '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="上架状态" width="100" align="center">
<template #default="{ row }">
<ElTag v-if="row.shelf_status === 1" type="success" size="small">上架</ElTag>
@@ -112,6 +127,25 @@
}}
</div>
</ElFormItem>
<ElFormItem label="生效条件">
<ElRadioGroup v-model="packageForm.expiry_base_override">
<ElRadio value="default">跟随套餐默认</ElRadio>
<ElRadio value="from_purchase">购买即生效</ElRadio>
<ElRadio value="from_realname">实名即生效</ElRadio>
</ElRadioGroup>
<div class="form-tip">仅影响后续新订单不影响已购买套餐</div>
</ElFormItem>
<template v-if="packageDialogType === 'edit'">
<ElFormItem label="套餐默认生效条件">
{{ packageForm.default_expiry_base_name || '-' }}
</ElFormItem>
<ElFormItem label="覆盖生效条件">
{{ packageForm.expiry_base_override_name || '-' }}
</ElFormItem>
<ElFormItem label="最终生效条件">
{{ packageForm.effective_expiry_base_name || '-' }}
</ElFormItem>
</template>
</ElForm>
<template #footer>
@@ -142,12 +176,24 @@
ElInputNumber,
ElForm,
ElFormItem,
ElRadioGroup,
ElRadio,
ElMessage,
FormInstance,
FormRules
} from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import { ShopSeriesGrantService, PackageManageService } from '@/api/modules'
import type { GrantPackageInfo, GrantPackageItem, PackageResponse } from '@/types/api'
import {
ShopSeriesGrantService,
ShopPackageAllocationService,
PackageManageService
} from '@/api/modules'
import type {
GrantPackageInfo,
GrantPackageItem,
PackageAllocationExpiryBaseOverride,
PackageResponse
} from '@/types/api'
import { useAuth } from '@/composables/useAuth'
defineOptions({ name: 'SeriesGrantPackages' })
@@ -163,6 +209,7 @@
const grantId = computed(() => Number(route.params.id))
const seriesId = ref<number>(0)
const shopId = ref<number>(0)
const grantName = ref<string>('')
const packageList = ref<GrantPackageInfo[]>([])
const availablePackages = ref<PackageResponse[]>([])
@@ -171,6 +218,8 @@
const packageDialogVisible = ref(false)
const packageDialogType = ref<'add' | 'edit'>('add')
type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
type PackageFormState = {
package_id?: number
package_name?: string
@@ -178,13 +227,21 @@
cost_price_yuan: number
original_cost_price?: number
suggested_retail_price?: number
allocation_id?: number
expiry_base_override: ExpiryBaseSelection
initial_expiry_base_override: ExpiryBaseSelection
default_expiry_base_name?: string | null
expiry_base_override_name?: string | null
effective_expiry_base_name?: string | null
}
const createDefaultPackageForm = (): PackageFormState => ({
package_id: undefined,
package_name: undefined,
package_code: undefined,
cost_price_yuan: 0
cost_price_yuan: 0,
expiry_base_override: 'default',
initial_expiry_base_override: 'default'
})
const packageForm = ref<PackageFormState>(createDefaultPackageForm())
@@ -266,6 +323,7 @@
if (res.code === 0) {
packageList.value = res.data.packages || []
seriesId.value = res.data.series_id
shopId.value = res.data.shop_id
grantName.value = `${res.data.series_name} - ${res.data.shop_name}`
}
} catch (error) {
@@ -293,7 +351,13 @@
package_code: row.package_code,
cost_price_yuan: row.cost_price / 100,
original_cost_price: packagePricing.original_cost_price,
suggested_retail_price: packagePricing.suggested_retail_price
suggested_retail_price: packagePricing.suggested_retail_price,
allocation_id: row.allocation_id,
expiry_base_override: row.expiry_base_override ?? 'default',
initial_expiry_base_override: row.expiry_base_override ?? 'default',
default_expiry_base_name: row.default_expiry_base_name,
expiry_base_override_name: row.expiry_base_override_name,
effective_expiry_base_name: row.effective_expiry_base_name
})
packageDialogVisible.value = true
nextTick(() => packageFormRef.value?.clearValidate())
@@ -369,14 +433,46 @@
if (Number.isNaN(currentCostPriceYuan)) {
throw new Error('当前成本价无效,无法保存')
}
const packages: GrantPackageItem[] = [
{
package_id: packageForm.value.package_id,
cost_price: Math.round(currentCostPriceYuan * 100)
const costPrice = Math.round(currentCostPriceYuan * 100)
if (packageDialogType.value === 'add') {
await ShopPackageAllocationService.createShopPackageAllocation({
shop_id: shopId.value,
package_id: packageForm.value.package_id!,
cost_price: costPrice,
expiry_base_override:
packageForm.value.expiry_base_override === 'default'
? null
: packageForm.value.expiry_base_override
})
} else {
const expiryBaseChanged =
packageForm.value.expiry_base_override !==
packageForm.value.initial_expiry_base_override
if (expiryBaseChanged && !packageForm.value.allocation_id) {
ElMessage.error('该套餐分配记录缺少ID无法更新生效条件')
return
}
]
await ShopSeriesGrantService.manageGrantPackages(grantId.value, { packages })
const packages: GrantPackageItem[] = [
{ package_id: packageForm.value.package_id, cost_price: costPrice }
]
await Promise.all([
ShopSeriesGrantService.manageGrantPackages(grantId.value, { packages }),
...(expiryBaseChanged
? [
ShopPackageAllocationService.updateShopPackageAllocationExpiryBase(
packageForm.value.allocation_id!,
{
expiry_base_override:
packageForm.value.expiry_base_override === 'default'
? null
: packageForm.value.expiry_base_override
}
)
]
: [])
])
}
packageDialogVisible.value = false
await fetchPackageList()
} catch (error) {