diff --git a/openspec/changes/add-asset-package-admin-adjustments/design.md b/openspec/changes/add-asset-package-admin-adjustments/design.md new file mode 100644 index 0000000..ac3a36d --- /dev/null +++ b/openspec/changes/add-asset-package-admin-adjustments/design.md @@ -0,0 +1,28 @@ +## Context + +The feature exposes high-impact package usage adjustments from the asset information package list. Because it can change real package consumption and expiration, the UI gate must combine deployment environment, account type, and production button permissions. + +## Goals / Non-Goals + +- Goals: Add two package-list operations, call the documented PATCH APIs, validate operator input, and refresh page data after success. +- Goals: Keep production access limited to platform users (`user_type=2`) with explicit button permissions. +- Goals: Keep test/development access limited to super admins (`user_type=1`) without requiring button permissions. +- Non-Goals: Add backend endpoints, change package list data retrieval, or change existing refund/package display behavior. + +## Decisions + +- Decision: Treat non-production as `import.meta.env.DEV` or a project-defined test mode discovered during implementation; production behavior MUST be used for production builds. +- Decision: Use separate permission codes for the two production-only buttons so operators can be granted one action without the other. +- Decision: Prefer small modal forms from the package list component unless an existing asset-operation dialog pattern provides a clearer reuse point. +- Decision: Send `data_usage_mb` as a non-negative integer MB value and `expires_at` as the selected formatted datetime string. + +## Risks / Trade-offs + +- Permission code names are not specified in the request. Implementation MUST confirm or define final codes before wiring production checks. +- Environment naming can vary by deployment. Implementation MUST inspect existing env conventions and avoid making test-only access available in production mode. +- Adjusting real used data can make displayed virtual/remaining values stale if the list is not refreshed. The UI MUST refresh after success. + +## Open Questions + +- What are the final production permission codes for “修改已用量” and “修改过期时间”? +- Which env flag identifies the requested “测试环境” if it is not `import.meta.env.DEV`? diff --git a/openspec/changes/add-asset-package-admin-adjustments/proposal.md b/openspec/changes/add-asset-package-admin-adjustments/proposal.md new file mode 100644 index 0000000..aee6ebb --- /dev/null +++ b/openspec/changes/add-asset-package-admin-adjustments/proposal.md @@ -0,0 +1,33 @@ +# Change: Add asset package admin adjustment actions + +## Why + +资产信息页的套餐列表目前只能查看套餐使用记录,缺少运营侧直接修正套餐真实已用量和过期时间的入口。后端已提供对应 PATCH 接口,前端需要补齐受环境、账号类型和按钮权限约束的操作能力。 + +## What Changes + +- 在资产信息页“套餐列表”的“操作”列新增两个操作: + - 修改已用量 + - 修改过期时间 +- 新增两个 PATCH 接口的前端契约: + - `PATCH /api/admin/assets/{identifier}/packages/{package_usage_id}/used-data` + - `PATCH /api/admin/assets/{identifier}/packages/{package_usage_id}/expires-at` +- 两个操作按钮使用环境差异化可见性规则: + - 线上环境:仅 `user_type=2` 且拥有对应按钮权限时显示 + - 测试/开发环境:仅 `user_type=1` 显示,不要求按钮权限 +- 为两个操作提供弹窗表单、输入校验、提交 loading、成功提示和刷新套餐列表行为。 +- 不改变套餐列表既有字段展示、退款入口或现有套餐流量展示口径。 + +## Impact + +- Affected specs: + - `asset-package-admin-adjustments` +- Affected code: + - `src/api/modules/asset.ts` + - `src/types/api/asset.ts` + - `src/views/asset-management/asset-information/components/PackageListCard.vue` + - Potential dialog component(s) under `src/views/asset-management/asset-information/components/dialogs/` +- Dependencies: + - Current asset identifier from asset information page context + - Current logged-in account `user_type` + - Button permissions for production visibility diff --git a/openspec/changes/add-asset-package-admin-adjustments/specs/asset-package-admin-adjustments/spec.md b/openspec/changes/add-asset-package-admin-adjustments/specs/asset-package-admin-adjustments/spec.md new file mode 100644 index 0000000..7e23d62 --- /dev/null +++ b/openspec/changes/add-asset-package-admin-adjustments/specs/asset-package-admin-adjustments/spec.md @@ -0,0 +1,134 @@ +## ADDED Requirements + +### Requirement: Package List Adjustment Actions + +The asset information package list SHALL provide operator actions to update a package usage record's real used data and expiration time. + +#### Scenario: Render adjustment actions in the package list + +- **GIVEN** 用户正在查看资产信息页的“套餐列表” +- **AND** 当前登录账号满足对应操作的可见性规则 +- **WHEN** 页面渲染套餐列表操作列 +- **THEN** 系统 MUST 显示“修改已用量”操作 +- **AND** 系统 MUST 显示“修改过期时间”操作 + +#### Scenario: Open used-data adjustment form + +- **GIVEN** 用户可见“修改已用量”操作 +- **WHEN** 用户点击某条套餐记录的“修改已用量” +- **THEN** 系统 MUST 打开修改已用量弹窗 +- **AND** 弹窗 MUST 展示当前套餐记录上下文 +- **AND** 表单 MUST 要求输入新的套餐真实已用量 MB + +#### Scenario: Open expiration adjustment form + +- **GIVEN** 用户可见“修改过期时间”操作 +- **WHEN** 用户点击某条套餐记录的“修改过期时间” +- **THEN** 系统 MUST 打开修改过期时间弹窗 +- **AND** 弹窗 MUST 展示当前套餐记录上下文 +- **AND** 表单 MUST 要求输入新的套餐过期时间 + +### Requirement: Used Data Update API Integration + +The frontend SHALL update a package usage record's real used data through `PATCH /api/admin/assets/{identifier}/packages/{package_usage_id}/used-data`. + +#### Scenario: Submit valid used-data update + +- **GIVEN** 用户正在修改某条套餐记录的已用量 +- **AND** 当前资产标识符为 `identifier` +- **AND** 当前套餐使用记录 ID 为 `package_usage_id` +- **WHEN** 用户输入非负整数 `data_usage_mb` 并提交 +- **THEN** 系统 MUST send `PATCH /api/admin/assets/{identifier}/packages/{package_usage_id}/used-data` +- **AND** 请求体 MUST be JSON with `data_usage_mb` +- **AND** `data_usage_mb` MUST be a non-negative integer MB value + +#### Scenario: Reject invalid used-data input + +- **GIVEN** 用户正在修改套餐已用量 +- **WHEN** 用户未输入值、输入负数或输入非整数 +- **THEN** 系统 MUST block submission +- **AND** 系统 MUST show a validation message explaining that the used data must be a non-negative integer MB value +- **AND** 系统 MUST NOT call the PATCH endpoint + +#### Scenario: Used-data update succeeds + +- **GIVEN** used-data PATCH request returns `code=0` +- **WHEN** 系统收到响应 +- **THEN** 系统 MUST show a success message +- **AND** 系统 MUST close the edit dialog +- **AND** 系统 MUST refresh the asset package list so the latest package usage data is displayed + +### Requirement: Expiration Time Update API Integration + +The frontend SHALL update a package usage record's expiration time through `PATCH /api/admin/assets/{identifier}/packages/{package_usage_id}/expires-at`. + +#### Scenario: Submit valid expiration update + +- **GIVEN** 用户正在修改某条套餐记录的过期时间 +- **AND** 当前资产标识符为 `identifier` +- **AND** 当前套餐使用记录 ID 为 `package_usage_id` +- **WHEN** 用户输入有效的 `expires_at` 并提交 +- **THEN** 系统 MUST send `PATCH /api/admin/assets/{identifier}/packages/{package_usage_id}/expires-at` +- **AND** 请求体 MUST be JSON with `expires_at` +- **AND** `expires_at` MUST be formatted as `YYYY-MM-DD HH:MM:SS` or another backend-accepted RFC3339 string + +#### Scenario: Reject missing expiration input + +- **GIVEN** 用户正在修改套餐过期时间 +- **WHEN** 用户未选择或未输入过期时间 +- **THEN** 系统 MUST block submission +- **AND** 系统 MUST show a validation message explaining that expiration time is required +- **AND** 系统 MUST NOT call the PATCH endpoint + +#### Scenario: Expiration update succeeds + +- **GIVEN** expires-at PATCH request returns `code=0` +- **WHEN** 系统收到响应 +- **THEN** 系统 MUST show a success message +- **AND** 系统 MUST close the edit dialog +- **AND** 系统 MUST refresh the asset package list so the latest expiration time is displayed + +### Requirement: Environment And Account Visibility Gate + +The frontend SHALL gate both package adjustment actions by environment, current account `user_type`, and production button permissions. + +#### Scenario: Production platform user with permission sees matching action + +- **GIVEN** 当前运行环境为线上环境 +- **AND** 当前登录账号的 `user_type` 为 `2` +- **AND** 当前登录账号拥有“修改已用量”的按钮权限 +- **WHEN** 页面渲染套餐列表操作列 +- **THEN** 系统 MUST show “修改已用量” + +#### Scenario: Production platform user without permission cannot see matching action + +- **GIVEN** 当前运行环境为线上环境 +- **AND** 当前登录账号的 `user_type` 为 `2` +- **AND** 当前登录账号缺少“修改过期时间”的按钮权限 +- **WHEN** 页面渲染套餐列表操作列 +- **THEN** 系统 MUST NOT show “修改过期时间” + +#### Scenario: Production super admin cannot see adjustment actions + +- **GIVEN** 当前运行环境为线上环境 +- **AND** 当前登录账号的 `user_type` 为 `1` +- **WHEN** 页面渲染套餐列表操作列 +- **THEN** 系统 MUST NOT show “修改已用量” +- **AND** 系统 MUST NOT show “修改过期时间” + +#### Scenario: Test environment super admin sees actions without permissions + +- **GIVEN** 当前运行环境为测试或开发环境 +- **AND** 当前登录账号的 `user_type` 为 `1` +- **AND** 当前登录账号没有两个调整操作的按钮权限 +- **WHEN** 页面渲染套餐列表操作列 +- **THEN** 系统 MUST show “修改已用量” +- **AND** 系统 MUST show “修改过期时间” + +#### Scenario: Test environment non-super-admin cannot see actions + +- **GIVEN** 当前运行环境为测试或开发环境 +- **AND** 当前登录账号的 `user_type` is not `1` +- **WHEN** 页面渲染套餐列表操作列 +- **THEN** 系统 MUST NOT show “修改已用量” +- **AND** 系统 MUST NOT show “修改过期时间” diff --git a/openspec/changes/add-asset-package-admin-adjustments/tasks.md b/openspec/changes/add-asset-package-admin-adjustments/tasks.md new file mode 100644 index 0000000..6193769 --- /dev/null +++ b/openspec/changes/add-asset-package-admin-adjustments/tasks.md @@ -0,0 +1,21 @@ +## 1. Implementation + +- [x] 1.1 Add API request/response types for package used-data and expires-at updates. +- [x] 1.2 Add AssetService PATCH methods for `/assets/{identifier}/packages/{package_usage_id}/used-data` and `/assets/{identifier}/packages/{package_usage_id}/expires-at`. +- [x] 1.3 Pass the current asset identifier to the package list component if it is not already available there. +- [x] 1.4 Add operation buttons to the package list with environment, `user_type`, and permission visibility rules. +- [x] 1.5 Add edit-used-data dialog/form with non-negative integer MB validation and submit handling. +- [x] 1.6 Add edit-expires-at dialog/form with required datetime validation and submit handling. +- [x] 1.7 Refresh package list data and show success feedback after each successful update. +- [x] 1.8 Run targeted lint/type checks for changed files and document any unrelated existing failures. + +## 2. Permission Setup + +- [x] 2.1 Confirm final production permission codes for “修改已用量” and “修改过期时间”. +- [x] 2.2 Wire the confirmed permission codes into button visibility checks. + +## 3. Validation + +- [x] 3.1 Verify production-mode behavior: `user_type=2` requires the matching permission for each button. +- [x] 3.2 Verify test/development-mode behavior: only `user_type=1` sees the buttons and permissions are ignored. +- [x] 3.3 Verify unauthorized account types do not see either operation. diff --git a/src/api/modules/asset.ts b/src/api/modules/asset.ts index 5ae7cd3..046c2b0 100644 --- a/src/api/modules/asset.ts +++ b/src/api/modules/asset.ts @@ -21,7 +21,10 @@ import type { UpdateAssetRealnameStatusRequest, DtoUpdateAssetRealnameStatusResponse, AssetOperationLogsResponse, - AssetOperationLogsParams + AssetOperationLogsParams, + AssetPackageUsageRecord, + UpdateAssetPackageUsedDataRequest, + UpdateAssetPackageExpiresAtRequest } from '@/types/api' export class AssetService extends BaseService { @@ -104,6 +107,36 @@ export class AssetService extends BaseService { ) } + /** + * 修改套餐真实已用量 + * PATCH /api/admin/assets/:identifier/packages/:package_usage_id/used-data + */ + static updateAssetPackageUsedData( + identifier: string, + packageUsageId: number, + data: UpdateAssetPackageUsedDataRequest + ): Promise> { + return this.patch>( + `/api/admin/assets/${identifier}/packages/${packageUsageId}/used-data`, + data as Record + ) + } + + /** + * 修改套餐过期时间 + * PATCH /api/admin/assets/:identifier/packages/:package_usage_id/expires-at + */ + static updateAssetPackageExpiresAt( + identifier: string, + packageUsageId: number, + data: UpdateAssetPackageExpiresAtRequest + ): Promise> { + return this.patch>( + `/api/admin/assets/${identifier}/packages/${packageUsageId}/expires-at`, + data as Record + ) + } + // ========== 资产停复机操作(统一接口)========== /** diff --git a/src/components/business/CreateRefundDialog.vue b/src/components/business/CreateRefundDialog.vue index 6d52d12..e676daa 100644 --- a/src/components/business/CreateRefundDialog.vue +++ b/src/components/business/CreateRefundDialog.vue @@ -36,7 +36,7 @@ - + o.order_no === props.initialOrderNo @@ -328,6 +332,9 @@ watch(dialogVisible, (val) => { if (val) { + if (hasInitialOrder.value && props.initialOrderId) { + formData.order_id = props.initialOrderId + } if (hasInitialOrder.value && props.initialOrderNo) { searchOrders(props.initialOrderNo) } else { diff --git a/src/types/api/asset.ts b/src/types/api/asset.ts index c81b779..cdcc64e 100644 --- a/src/types/api/asset.ts +++ b/src/types/api/asset.ts @@ -270,6 +270,14 @@ export interface AssetPackageParams { page_size?: number // 每页数量,默认50 } +export interface UpdateAssetPackageUsedDataRequest { + data_usage_mb: number | null // 新的套餐真实已用量(MB) +} + +export interface UpdateAssetPackageExpiresAtRequest { + expires_at: string // 新的套餐过期时间,格式:YYYY-MM-DD HH:MM:SS 或 RFC3339 +} + /** * 当前套餐响应(结构同套餐使用记录单项) * 对应接口:GET /api/admin/assets/:asset_type/:id/current-package diff --git a/src/types/auto-imports.d.ts b/src/types/auto-imports.d.ts index 73b1a90..54ac12e 100644 --- a/src/types/auto-imports.d.ts +++ b/src/types/auto-imports.d.ts @@ -8,7 +8,7 @@ export {} declare global { const EffectScope: typeof import('vue')['EffectScope'] const ElButton: (typeof import('element-plus/es'))['ElButton'] - const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox'] + const ElMessageBox: typeof import('element-plus/es')['ElMessageBox'] const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] diff --git a/src/types/components.d.ts b/src/types/components.d.ts index 86f45fc..50e1527 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -83,7 +83,55 @@ declare module 'vue' { CreateRefundDialog: typeof import('./../components/business/CreateRefundDialog.vue')['default'] CustomerAccountDialog: typeof import('./../components/business/CustomerAccountDialog.vue')['default'] DetailPage: typeof import('./../components/common/DetailPage.vue')['default'] + ElAlert: typeof import('element-plus/es')['ElAlert'] + ElAvatar: typeof import('element-plus/es')['ElAvatar'] + ElButton: typeof import('element-plus/es')['ElButton'] + ElCalendar: typeof import('element-plus/es')['ElCalendar'] + ElCard: typeof import('element-plus/es')['ElCard'] + ElCascader: typeof import('element-plus/es')['ElCascader'] + ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] + ElCol: typeof import('element-plus/es')['ElCol'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] + ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] + ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] + ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] + ElDialog: typeof import('element-plus/es')['ElDialog'] + ElDivider: typeof import('element-plus/es')['ElDivider'] + ElDrawer: typeof import('element-plus/es')['ElDrawer'] + ElDropdown: typeof import('element-plus/es')['ElDropdown'] + ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] + ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] + ElEmpty: typeof import('element-plus/es')['ElEmpty'] + ElForm: typeof import('element-plus/es')['ElForm'] + ElFormItem: typeof import('element-plus/es')['ElFormItem'] + ElIcon: typeof import('element-plus/es')['ElIcon'] + ElInput: typeof import('element-plus/es')['ElInput'] + ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] + ElMenu: typeof import('element-plus/es')['ElMenu'] + ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] + ElOption: typeof import('element-plus/es')['ElOption'] + ElPagination: typeof import('element-plus/es')['ElPagination'] + ElPopover: typeof import('element-plus/es')['ElPopover'] + ElProgress: typeof import('element-plus/es')['ElProgress'] + ElRadio: typeof import('element-plus/es')['ElRadio'] + ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] + ElRow: typeof import('element-plus/es')['ElRow'] + ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] + ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] + ElSwitch: typeof import('element-plus/es')['ElSwitch'] + ElTable: typeof import('element-plus/es')['ElTable'] + ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] + ElTabPane: typeof import('element-plus/es')['ElTabPane'] + ElTabs: typeof import('element-plus/es')['ElTabs'] + ElTag: typeof import('element-plus/es')['ElTag'] + ElTimeline: typeof import('element-plus/es')['ElTimeline'] + ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem'] + ElTooltip: typeof import('element-plus/es')['ElTooltip'] + ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] + ElUpload: typeof import('element-plus/es')['ElUpload'] + ElWatermark: typeof import('element-plus/es')['ElWatermark'] HorizontalSubmenu: typeof import('./../components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue')['default'] ImportDialog: typeof import('./../components/business/ImportDialog.vue')['default'] LoginLeftView: typeof import('./../components/core/views/login/LoginLeftView.vue')['default'] @@ -107,4 +155,7 @@ declare module 'vue' { ThemeSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ThemeSettings.vue')['default'] UpdateRealnameStatusDialog: typeof import('./../components/business/UpdateRealnameStatusDialog.vue')['default'] } + export interface ComponentCustomProperties { + vLoading: typeof import('element-plus/es')['ElLoadingDirective'] + } } diff --git a/src/views/asset-management/asset-information/components/PackageListCard.vue b/src/views/asset-management/asset-information/components/PackageListCard.vue index 5755d14..b036269 100644 --- a/src/views/asset-management/asset-information/components/PackageListCard.vue +++ b/src/views/asset-management/asset-information/components/PackageListCard.vue @@ -200,36 +200,54 @@ {{ formatDateTime(scope.row.created_at) }} - + @@ -250,14 +268,96 @@ + + + + + {{ selectedPackage?.package_name || '-' }} + + + {{ formatDataSize(selectedPackage?.real_used_mb || 0) }} + + +
+ + + + + +
+
+
+ +
+ + + + + {{ selectedPackage?.package_name || '-' }} + + + {{ formatDateTime(selectedPackage?.expires_at) }} + + + + + + +