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

@@ -0,0 +1,32 @@
# Change: 支持套餐分配生效条件覆盖
## Why
代理套餐分配目前只能使用套餐本身的默认生效条件,运营无法针对某个店铺套餐授权选择购买即生效或实名即生效。分配页也无法区分套餐默认值、分配覆盖值和最终实际生效值,容易误解后续订单的生效规则。
## What Changes
- 在套餐授权/分配和已分配套餐编辑入口增加“生效条件”单选:跟随套餐默认、购买即生效、实名即生效。
- 新建分配时调用 `POST /api/admin/shop-package-allocations`,明确传递 `expiry_base_override`;选择“跟随套餐默认”时 MUST 显式传递 `null`,不得省略字段。
- 已分配套餐修改生效条件时调用 `PATCH /api/admin/shop-package-allocations/{id}/expiry-base`,请求体为 `expiry_base_override:null|from_purchase|from_realname`
- 在分配详情、编辑弹框和代理系列授权套餐列表展示后端返回的默认生效条件、覆盖生效条件和最终生效条件的中文名称。
- 修改弹框提示“仅影响后续新订单,不影响已购买套餐”。
- 前端不根据默认值和覆盖值自行计算最终生效条件,直接使用后端返回的 `effective_expiry_base` 及其名称字段。
## Impact
- Affected specs:
- `shop-package-allocation`
- `shop-series-grant-packages`
- Affected code:
- `src/types/api/packageManagement.ts`
- 新增或扩展 `src/api/modules` 中的店铺套餐分配服务
- `src/views/package-management/series-grants/index.vue`
- `src/views/package-management/series-grants/packages.vue`
- `src/views/package-management/series-grants/detail.vue`
- API contracts:
- `POST /api/admin/shop-package-allocations`
- `PATCH /api/admin/shop-package-allocations/{id}/expiry-base`
- Out of scope:
- 已购买套餐或历史订单的生效条件迁移
- 前端根据分配字段推导或覆盖实际生效条件

View File

@@ -0,0 +1,74 @@
## ADDED Requirements
### Requirement: Package Allocation Expiry Base Override Contract
The shop package allocation contract SHALL support an explicit `expiry_base_override` when creating or updating an allocation. The allowed request values MUST be `null`, `from_purchase`, and `from_realname`.
#### Scenario: Create allocation following the package default
- **GIVEN** 用户在套餐授权/分配弹框选择“跟随套餐默认”
- **WHEN** 用户创建店铺套餐分配
- **THEN** 系统 MUST call `POST /api/admin/shop-package-allocations`
- **AND** 请求 MUST explicitly contain `expiry_base_override: null`
- **AND** 系统 MUST NOT omit `expiry_base_override` to represent following the default
#### Scenario: Create allocation with purchase activation override
- **GIVEN** 用户在套餐授权/分配弹框选择“购买即生效”
- **WHEN** 用户创建店铺套餐分配
- **THEN** 请求 MUST contain `expiry_base_override: "from_purchase"`
#### Scenario: Create allocation with realname activation override
- **GIVEN** 用户在套餐授权/分配弹框选择“实名即生效”
- **WHEN** 用户创建店铺套餐分配
- **THEN** 请求 MUST contain `expiry_base_override: "from_realname"`
### Requirement: Package Allocation Expiry Base Update
The system SHALL update an existing package allocation's expiry-base override through `PATCH /api/admin/shop-package-allocations/{id}/expiry-base` with a body containing `expiry_base_override`.
#### Scenario: Update allocation expiry base override
- **GIVEN** 用户正在编辑已分配套餐的生效条件
- **WHEN** 用户选择“购买即生效”或“实名即生效”并保存
- **THEN** 系统 MUST call `PATCH /api/admin/shop-package-allocations/{id}/expiry-base`
- **AND** 请求体 MUST contain the selected `expiry_base_override`
#### Scenario: Restore allocation to package default
- **GIVEN** 用户正在编辑已分配套餐的生效条件
- **WHEN** 用户选择“跟随套餐默认”并保存
- **THEN** 请求体 MUST explicitly contain `expiry_base_override: null`
### Requirement: Package Allocation Expiry Base Resolution Response
The package allocation response SHALL provide `default_expiry_base`, `expiry_base_override`, and `effective_expiry_base`, together with their corresponding Chinese name fields. The frontend MUST render the returned effective value and name without calculating it from other fields.
#### Scenario: Display distinct expiry base values
- **WHEN** 前端加载或更新店铺套餐分配记录
- **THEN** 前端类型 MUST support `default_expiry_base``expiry_base_override``effective_expiry_base`
- **AND** 前端类型 MUST support the corresponding Chinese name fields
- **AND** 页面 MUST distinguish the default value, override value, and effective value
#### Scenario: Use backend effective expiry base
- **GIVEN** 分配响应同时返回默认值和覆盖值
- **WHEN** 页面展示最终生效条件
- **THEN** 页面 MUST display the backend returned `effective_expiry_base_name`
- **AND** 前端 MUST NOT calculate or replace the effective value from the default value and override value
### Requirement: Package Allocation Expiry Base Applies Prospectively
The package allocation expiry-base override SHALL apply only to future new orders. The allocation creation and editing UI MUST inform the user that purchased packages are unaffected.
#### Scenario: Inform user before changing allocation expiry base
- **WHEN** 用户在已分配套餐编辑弹框查看或修改生效条件
- **THEN** 页面 MUST display `仅影响后续新订单,不影响已购买套餐`
#### Scenario: Preserve purchased package behavior
- **WHEN** 用户创建、更新或恢复套餐分配的生效条件
- **THEN** 前端 MUST NOT attempt to update existing purchased packages or historical orders

View File

@@ -0,0 +1,35 @@
## ADDED Requirements
### Requirement: Series Grant Package Expiry Base Visibility
The series grant package list and package edit context SHALL display the package allocation's default expiry base, override expiry base, and effective expiry base using the Chinese name fields returned by the backend.
#### Scenario: Display expiry bases in series grant package list
- **GIVEN** 用户进入代理系列授权套餐列表
- **WHEN** 已授权套餐记录返回默认、覆盖和最终生效条件字段
- **THEN** 页面 MUST display the default expiry base name, override expiry base name, and effective expiry base name for the package allocation
#### Scenario: Display expiry bases in package edit context
- **GIVEN** 用户打开代理系列授权中已授权套餐的编辑弹框
- **WHEN** 套餐分配数据加载完成
- **THEN** 页面 MUST display the default expiry base name, override expiry base name, and effective expiry base name
- **AND** 页面 MUST display `仅影响后续新订单,不影响已购买套餐`
### Requirement: Series Grant Package Expiry Base Editing
The series grant package edit context SHALL provide `跟随套餐默认`, `购买即生效`, and `实名即生效` options, then update the corresponding package allocation record through the package allocation expiry-base API.
#### Scenario: Edit override from series grant package list
- **GIVEN** 用户正在代理系列授权套餐列表编辑已授权套餐
- **WHEN** 用户选择生效条件并保存
- **THEN** 系统 MUST use the corresponding package allocation record ID to call `PATCH /api/admin/shop-package-allocations/{id}/expiry-base`
- **AND** 选择“跟随套餐默认”时请求 MUST explicitly contain `expiry_base_override: null`
#### Scenario: Refresh effective value after editing
- **WHEN** 代理系列授权套餐的生效条件更新成功
- **THEN** 页面 MUST refresh the relevant package allocation data
- **AND** 页面 MUST display the latest backend returned effective expiry base name

View File

@@ -0,0 +1,26 @@
## 1. Allocation Contract
- [x] 1.1 扩展店铺套餐分配请求与响应类型,支持 `expiry_base_override``default_expiry_base``effective_expiry_base` 及对应中文名称字段。
- [x] 1.2 接入创建分配接口 `POST /api/admin/shop-package-allocations``expiry_base_override` 参数。
- [x] 1.3 接入修改生效条件接口 `PATCH /api/admin/shop-package-allocations/{id}/expiry-base`
## 2. Allocation Expiry Condition UI
- [x] 2.1 在套餐授权/分配弹框提供“跟随套餐默认”“购买即生效”“实名即生效”三选一。
- [x] 2.2 选择“跟随套餐默认”时显式提交 `expiry_base_override: null`
- [x] 2.3 在已分配套餐编辑弹框显示“仅影响后续新订单,不影响已购买套餐”提示。
- [x] 2.4 在已分配套餐详情或编辑弹框显示默认值、覆盖值和最终值的后端中文名称。
## 3. Series Grant Package Visibility
- [x] 3.1 在代理系列授权套餐列表显示默认生效条件、覆盖生效条件和最终生效条件。
- [x] 3.2 从代理系列授权套餐列表编辑生效条件时,使用对应套餐分配记录 ID 调用生效条件更新接口。
- [x] 3.3 保存成功后刷新授权套餐列表或详情,展示后端返回的最新最终生效条件。
## 4. Policy Integrity and Verification
- [x] 4.1 前端不从默认值和覆盖值计算或修改 `effective_expiry_base`
- [x] 4.2 验证新建分配时三种选项分别提交 `null``from_purchase``from_realname`
- [x] 4.3 验证编辑覆盖值和恢复默认均只影响后续新订单,不影响已购买套餐。
- [x] 4.4 验证页面可区分默认值、覆盖值和最终生效条件,并正确显示后端中文名称。
- [x] 4.5 运行相关前端校验,并执行 `openspec validate update-package-allocation-expiry-base-override --strict`

View File

@@ -18,6 +18,7 @@ export { CarrierService } from './carrier'
export { PackageSeriesService } from './packageSeries' export { PackageSeriesService } from './packageSeries'
export { PackageManageService } from './packageManage' export { PackageManageService } from './packageManage'
export { ShopSeriesGrantService } from './shopSeriesGrant' export { ShopSeriesGrantService } from './shopSeriesGrant'
export { ShopPackageAllocationService } from './shopPackageAllocation'
export { OrderService } from './order' export { OrderService } from './order'
export { AssetService } from './asset' export { AssetService } from './asset'
export { AgentRechargeService } from './agentRecharge' 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 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_id: number
package_name?: string package_name?: string
package_code?: string package_code?: string

View File

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

View File

@@ -88,17 +88,8 @@
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="统计范围" width="120"> <ElTableColumn label="统计范围" width="120">
<template #default="{ row }"> <template #default>
<ElTag size="small" type="warning"> <ElTag size="small" type="warning">仅自己</ElTag>
仅自己
<!--{{-->
<!-- row.stat_scope === 'self'-->
<!-- ? '仅自己'-->
<!-- : row.stat_scope === 'self_and_sub'-->
<!-- ? '自己+下级'-->
<!-- : '-'-->
<!--}}-->
</ElTag>
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="佣金金额" min-width="100"> <ElTableColumn label="佣金金额" min-width="100">
@@ -160,6 +151,21 @@
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span> <span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
</template> </template>
</ElTableColumn> </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"> <ElTableColumn label="上架状态" align="center">
<template #default="{ row }"> <template #default="{ row }">
<ElTag v-if="row.shelf_status === 1" type="success" size="small">上架</ElTag> <ElTag v-if="row.shelf_status === 1" type="success" size="small">上架</ElTag>
@@ -268,6 +274,25 @@
}} }}
</div> </div>
</ElFormItem> </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> </ElForm>
<template #footer> <template #footer>
@@ -304,16 +329,24 @@
ElInputNumber, ElInputNumber,
ElForm, ElForm,
ElFormItem, ElFormItem,
ElRadioGroup,
ElRadio,
FormInstance, FormInstance,
FormRules FormRules
} from 'element-plus' } from 'element-plus'
import { ArrowLeft, Loading, Setting, List, Plus } from '@element-plus/icons-vue' import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import { ShopSeriesGrantService, PackageManageService, PackageSeriesService } from '@/api/modules' import {
ShopSeriesGrantService,
ShopPackageAllocationService,
PackageManageService,
PackageSeriesService
} from '@/api/modules'
import type { import type {
ShopSeriesGrantResponse, ShopSeriesGrantResponse,
PackageResponse, PackageResponse,
GrantPackageItem, GrantPackageItem,
GrantPackageInfo, GrantPackageInfo,
PackageAllocationExpiryBaseOverride,
PackageSeriesResponse PackageSeriesResponse
} from '@/types/api' } from '@/types/api'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
@@ -334,6 +367,8 @@
const availablePackages = ref<PackageResponse[]>([]) const availablePackages = ref<PackageResponse[]>([])
const packageFormRef = ref<FormInstance>() const packageFormRef = ref<FormInstance>()
type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
// 套餐表单 // 套餐表单
const packageForm = ref<{ const packageForm = ref<{
package_id?: number package_id?: number
@@ -341,8 +376,16 @@
package_code?: string package_code?: string
original_cost_price?: number original_cost_price?: number
cost_price_yuan: 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_name: undefined,
package_code: undefined, package_code: undefined,
original_cost_price: 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_name: row.package_name,
package_code: row.package_code, package_code: row.package_code,
original_cost_price: row.cost_price / 100, 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 packageDialogVisible.value = true
@@ -539,23 +590,48 @@
submitLoading.value = true submitLoading.value = true
try { try {
const packages: GrantPackageItem[] = []
if (packageDialogType.value === 'add') { if (packageDialogType.value === 'add') {
// 添加模式:添加新套餐 await ShopPackageAllocationService.createShopPackageAllocation({
packages.push({ shop_id: detailData.value!.shop_id,
package_id: packageForm.value.package_id, package_id: packageForm.value.package_id!,
cost_price: Math.round(packageForm.value.cost_price_yuan * 100) 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 { } else {
// 编辑模式:更新套餐成本价 const expiryBaseChanged =
packages.push({ packageForm.value.expiry_base_override !==
package_id: packageForm.value.package_id, packageForm.value.initial_expiry_base_override
cost_price: Math.round(packageForm.value.cost_price_yuan * 100) 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' ? '添加成功' : '更新成功') ElMessage.success(packageDialogType.value === 'add' ? '添加成功' : '更新成功')
packageDialogVisible.value = false packageDialogVisible.value = false
@@ -613,7 +689,9 @@
package_name: undefined, package_name: undefined,
package_code: undefined, package_code: undefined,
original_cost_price: undefined, original_cost_price: undefined,
cost_price_yuan: 0 cost_price_yuan: 0,
expiry_base_override: 'default',
initial_expiry_base_override: 'default'
} }
availablePackages.value = [] availablePackages.value = []
} }

View File

@@ -170,6 +170,13 @@
{{ formatSuggestedRetailPrice(pkg.suggested_retail_price) }}) {{ formatSuggestedRetailPrice(pkg.suggested_retail_price) }})
</span> </span>
</div> </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 type="danger" size="small" @click="removePackage(index)"
>删除</ElButton >删除</ElButton
> >
@@ -178,6 +185,7 @@
<div class="form-tip" <div class="form-tip"
>设置每个套餐的成本价单位不能低于成本价不能高于建议售价的50%溢价</div >设置每个套餐的成本价单位不能低于成本价不能高于建议售价的50%溢价</div
> >
<div class="form-tip">生效条件仅影响后续新订单不影响已购买套餐</div>
</ElFormItem> </ElFormItem>
</template> </template>
</div> </div>
@@ -253,7 +261,7 @@
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="统计范围" width="140"> <ElTableColumn label="统计范围" width="140">
<template #default="{ row }"> <template #default>
<ElTag size="small" type="warning"> 仅自己 </ElTag> <ElTag size="small" type="warning"> 仅自己 </ElTag>
</template> </template>
</ElTableColumn> </ElTableColumn>
@@ -427,7 +435,7 @@
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="统计范围" width="140"> <ElTableColumn label="统计范围" width="140">
<template #default="{ row }"> <template #default>
<ElTag size="small" type="warning"> 仅自己 </ElTag> <ElTag size="small" type="warning"> 仅自己 </ElTag>
</template> </template>
</ElTableColumn> </ElTableColumn>
@@ -546,6 +554,7 @@
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { import {
ShopSeriesGrantService, ShopSeriesGrantService,
ShopPackageAllocationService,
PackageSeriesService, PackageSeriesService,
ShopService, ShopService,
PackageManageService PackageManageService
@@ -558,8 +567,7 @@
ElRow, ElRow,
ElCol, ElCol,
ElSteps, ElSteps,
ElStep, ElStep
ElEmpty
} from 'element-plus' } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { import type {
@@ -567,7 +575,8 @@
PackageSeriesResponse, PackageSeriesResponse,
ShopResponse, ShopResponse,
PackageResponse, PackageResponse,
CommissionTier CommissionTier,
PackageAllocationExpiryBaseOverride
} from '@/types/api' } from '@/types/api'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
@@ -604,8 +613,6 @@
const currentStep = ref(0) // 当前步骤0 为第一步1 为第二步 const currentStep = ref(0) // 当前步骤0 为第一步1 为第二步
const seriesOptions = ref<PackageSeriesResponse[]>([]) const seriesOptions = ref<PackageSeriesResponse[]>([])
const shopOptions = ref<ShopResponse[]>([])
const shopTreeData = ref<ShopResponse[]>([])
const packageOptions = ref<PackageResponse[]>([]) const packageOptions = ref<PackageResponse[]>([])
const selectedPackageIds = ref<number[]>([]) const selectedPackageIds = ref<number[]>([])
const searchSeriesOptions = ref<PackageSeriesResponse[]>([]) const searchSeriesOptions = ref<PackageSeriesResponse[]>([])
@@ -777,6 +784,7 @@
cost_price: number cost_price: number
original_cost_price?: number original_cost_price?: number
suggested_retail_price?: number suggested_retail_price?: number
expiry_base_override: PackageAllocationExpiryBaseOverride | 'default'
}> // 套餐配置 }> // 套餐配置
}) })
@@ -1027,39 +1035,6 @@
return actions 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(() => { onMounted(() => {
getTableData() getTableData()
}) })
@@ -1170,7 +1145,8 @@
suggested_retail_price: suggested_retail_price:
pkg?.suggested_retail_price !== undefined && pkg?.suggested_retail_price !== null pkg?.suggested_retail_price !== undefined && pkg?.suggested_retail_price !== null
? pkg.suggested_retail_price / 100 ? pkg.suggested_retail_price / 100
: undefined : undefined,
expiry_base_override: 'default'
}) })
} }
}) })
@@ -1597,7 +1573,8 @@
pkgOption?.suggested_retail_price !== undefined && pkgOption?.suggested_retail_price !== undefined &&
pkgOption?.suggested_retail_price !== null pkgOption?.suggested_retail_price !== null
? pkgOption.suggested_retail_price / 100 ? pkgOption.suggested_retail_price / 100
: undefined : undefined,
expiry_base_override: pkg.expiry_base_override ?? 'default'
} }
}) })
selectedPackageIds.value = detail.packages.map((pkg) => pkg.package_id) selectedPackageIds.value = detail.packages.map((pkg) => pkg.package_id)
@@ -1745,8 +1722,15 @@
data.force_recharge_amount = 0 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) { for (const pkg of form.packages) {
const pkgInfo = packageOptions.value.find((p) => p.id === pkg.package_id) const pkgInfo = packageOptions.value.find((p) => p.id === pkg.package_id)
@@ -1778,11 +1762,6 @@
return 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') { if (dialogType.value === 'add') {
@@ -1790,6 +1769,19 @@
data.series_id = form.series_id data.series_id = form.series_id
data.shop_id = form.shop_id data.shop_id = form.shop_id
await ShopSeriesGrantService.createShopSeriesGrant(data) 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('新增成功') ElMessage.success('新增成功')
} else { } else {
// 编辑时调用更新接口 // 编辑时调用更新接口

View File

@@ -40,6 +40,21 @@
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span> <span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
</template> </template>
</ElTableColumn> </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"> <ElTableColumn label="上架状态" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
<ElTag v-if="row.shelf_status === 1" type="success" size="small">上架</ElTag> <ElTag v-if="row.shelf_status === 1" type="success" size="small">上架</ElTag>
@@ -112,6 +127,25 @@
}} }}
</div> </div>
</ElFormItem> </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> </ElForm>
<template #footer> <template #footer>
@@ -142,12 +176,24 @@
ElInputNumber, ElInputNumber,
ElForm, ElForm,
ElFormItem, ElFormItem,
ElRadioGroup,
ElRadio,
ElMessage,
FormInstance, FormInstance,
FormRules FormRules
} from 'element-plus' } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue' import { ArrowLeft } from '@element-plus/icons-vue'
import { ShopSeriesGrantService, PackageManageService } from '@/api/modules' import {
import type { GrantPackageInfo, GrantPackageItem, PackageResponse } from '@/types/api' ShopSeriesGrantService,
ShopPackageAllocationService,
PackageManageService
} from '@/api/modules'
import type {
GrantPackageInfo,
GrantPackageItem,
PackageAllocationExpiryBaseOverride,
PackageResponse
} from '@/types/api'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
defineOptions({ name: 'SeriesGrantPackages' }) defineOptions({ name: 'SeriesGrantPackages' })
@@ -163,6 +209,7 @@
const grantId = computed(() => Number(route.params.id)) const grantId = computed(() => Number(route.params.id))
const seriesId = ref<number>(0) const seriesId = ref<number>(0)
const shopId = ref<number>(0)
const grantName = ref<string>('') const grantName = ref<string>('')
const packageList = ref<GrantPackageInfo[]>([]) const packageList = ref<GrantPackageInfo[]>([])
const availablePackages = ref<PackageResponse[]>([]) const availablePackages = ref<PackageResponse[]>([])
@@ -171,6 +218,8 @@
const packageDialogVisible = ref(false) const packageDialogVisible = ref(false)
const packageDialogType = ref<'add' | 'edit'>('add') const packageDialogType = ref<'add' | 'edit'>('add')
type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
type PackageFormState = { type PackageFormState = {
package_id?: number package_id?: number
package_name?: string package_name?: string
@@ -178,13 +227,21 @@
cost_price_yuan: number cost_price_yuan: number
original_cost_price?: number original_cost_price?: number
suggested_retail_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 => ({ const createDefaultPackageForm = (): PackageFormState => ({
package_id: undefined, package_id: undefined,
package_name: undefined, package_name: undefined,
package_code: 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()) const packageForm = ref<PackageFormState>(createDefaultPackageForm())
@@ -266,6 +323,7 @@
if (res.code === 0) { if (res.code === 0) {
packageList.value = res.data.packages || [] packageList.value = res.data.packages || []
seriesId.value = res.data.series_id seriesId.value = res.data.series_id
shopId.value = res.data.shop_id
grantName.value = `${res.data.series_name} - ${res.data.shop_name}` grantName.value = `${res.data.series_name} - ${res.data.shop_name}`
} }
} catch (error) { } catch (error) {
@@ -293,7 +351,13 @@
package_code: row.package_code, package_code: row.package_code,
cost_price_yuan: row.cost_price / 100, cost_price_yuan: row.cost_price / 100,
original_cost_price: packagePricing.original_cost_price, 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 packageDialogVisible.value = true
nextTick(() => packageFormRef.value?.clearValidate()) nextTick(() => packageFormRef.value?.clearValidate())
@@ -369,14 +433,46 @@
if (Number.isNaN(currentCostPriceYuan)) { if (Number.isNaN(currentCostPriceYuan)) {
throw new Error('当前成本价无效,无法保存') throw new Error('当前成本价无效,无法保存')
} }
const packages: GrantPackageItem[] = [ const costPrice = Math.round(currentCostPriceYuan * 100)
{ if (packageDialogType.value === 'add') {
package_id: packageForm.value.package_id, await ShopPackageAllocationService.createShopPackageAllocation({
cost_price: Math.round(currentCostPriceYuan * 100) 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 packageDialogVisible.value = false
await fetchPackageList() await fetchPackageList()
} catch (error) { } catch (error) {