diff --git a/docs/权限.md b/docs/权限.md index 126f7fb..aed54b9 100644 --- a/docs/权限.md +++ b/docs/权限.md @@ -149,6 +149,7 @@ | 申请详情 | `/finance/employee-collection/applications/detail/:id` | `applicationDetail` | `employee_collection:application_detail` | | 修改并重新提交 | — | `applicationUpdate` | `employee_collection:application_update` | | 查看凭证 | — | `applicationViewVoucher` | `employee_collection:application_view_voucher` | +| 恢复审批 | — | `applicationRecoverApproval` | `employee_collection:application_recover_approval` | ## 财务管理:收款方式管理 diff --git a/openspec/changes/update-business-user-group-members-contract/proposal.md b/openspec/changes/update-business-user-group-members-contract/proposal.md new file mode 100644 index 0000000..f0adc16 --- /dev/null +++ b/openspec/changes/update-business-user-group-members-contract/proposal.md @@ -0,0 +1,19 @@ +# Change: 对齐业务用户组成员增量管理接口 + +## Why + +《产品迭代8月份/修改.md》已将业务用户组成员维护从“整体替换”调整为增量增加/迁移和按组移除。当前前端仍调用旧 PUT/全局 DELETE 接口,无法展示候选成员归属和操作提示。 + +## What Changes + +- 对齐成员查询、成员候选、增量增加/迁移、按组移除接口。 +- 成员管理弹窗展示当前成员及成员关系信息。 +- 候选账号展示 `add`、`already_member`、`move` 操作提示。 +- 停用组允许查询和移除,但新增/迁移由后端拒绝并提示。 +- 用户组列表/详情使用实时成员数。 + +## Impact + +- Affected specs: `business-user-group-management` +- Affected code: business user group types/API/page +- Breaking API alignment: 移除前端对旧整体替换 PUT 与全局清空 DELETE 的调用。 diff --git a/openspec/changes/update-business-user-group-members-contract/specs/business-user-group-management/spec.md b/openspec/changes/update-business-user-group-members-contract/specs/business-user-group-management/spec.md new file mode 100644 index 0000000..b6a2264 --- /dev/null +++ b/openspec/changes/update-business-user-group-members-contract/specs/business-user-group-management/spec.md @@ -0,0 +1,17 @@ +## ADDED Requirements + +### Requirement: Incremental Business User Group Membership + +The frontend SHALL use the member query, candidate query, incremental POST add/move, and scoped DELETE remove contracts from the modification document. + +#### Scenario: Add or move members + +- **WHEN** the user selects candidate accounts and confirms adding members +- **THEN** the frontend MUST POST the account IDs to `/api/admin/business-user-groups/{id}/members` +- **AND** MUST refresh current members and member counts + +#### Scenario: Remove current members + +- **WHEN** the user selects members currently belonging to the group and confirms removal +- **THEN** the frontend MUST DELETE `/api/admin/business-user-groups/{id}/members` with the account IDs +- **AND** MUST refresh current members diff --git a/openspec/changes/update-business-user-group-members-contract/tasks.md b/openspec/changes/update-business-user-group-members-contract/tasks.md new file mode 100644 index 0000000..a3af25b --- /dev/null +++ b/openspec/changes/update-business-user-group-members-contract/tasks.md @@ -0,0 +1,14 @@ +## 1. Contract + +- [ ] 1.1 更新成员、候选和成员数类型。 +- [ ] 1.2 更新 `BusinessUserGroupService` 方法与 HTTP 方法/路径。 + +## 2. UI + +- [ ] 2.1 展示当前成员列表和成员状态。 +- [ ] 2.2 使用候选接口展示归属筛选和操作提示。 +- [ ] 2.3 将“整体替换”改为增量添加/迁移,将移除限定为当前组成员。 + +## 3. Verification + +- [ ] 3.1 运行类型检查、lint、编码检查、构建和 OpenSpec 校验。 diff --git a/openspec/changes/update-employee-collection-approval-recovery/design.md b/openspec/changes/update-employee-collection-approval-recovery/design.md new file mode 100644 index 0000000..dd9f7c6 --- /dev/null +++ b/openspec/changes/update-employee-collection-approval-recovery/design.md @@ -0,0 +1,9 @@ +## Context + +核销申请的审批提交状态与业务状态独立维护。恢复操作只能复用最新审批尝试和原审批实例,不能创建新的申请或审批尝试。 + +## Decisions + +- 资格以响应中的 `recoverable` 为准,前端不根据状态字段自行推导。 +- 列表提供快速入口,详情提供主要操作入口;两处共用同一恢复权限编码。 +- 恢复接口只传申请 ID,结果由后端返回并触发重新加载。 diff --git a/openspec/changes/update-employee-collection-approval-recovery/proposal.md b/openspec/changes/update-employee-collection-approval-recovery/proposal.md new file mode 100644 index 0000000..90e0ec7 --- /dev/null +++ b/openspec/changes/update-employee-collection-approval-recovery/proposal.md @@ -0,0 +1,25 @@ +# Change: 补齐核销申请恢复审批 + +## Why + +《产品迭代8月份/修改.md》新增核销申请恢复原审批接口和恢复资格字段。当前前端类型、列表和详情尚未提供恢复入口,提交失败或结果未知的核销申请无法由运营人员处理。 + +## What Changes + +- 扩展核销申请响应类型,支持 `recoverable`、提交状态、失败摘要和最近恢复时间。 +- 新增 `EmployeeCollectionService.recoverApplicationApproval(id)`。 +- 核销申请列表和详情在 `recoverable = true` 且具备权限时显示“恢复审批”。 +- 恢复成功或冲突后刷新详情/列表,并展示后端错误消息。 +- 新增权限 `employee_collection:application_recover_approval` 并写入权限文档。 + +## Impact + +- Affected specs: `employee-collection` +- Affected code: + - `src/types/api/employeeCollection.ts` + - `src/api/modules/employeeCollection.ts` + - `src/config/constants/augustIteration.ts` + - `src/views/finance/employee-collection/applications/index.vue` + - `src/views/finance/employee-collection/applications/detail.vue` + - `docs/权限.md` +- Out of scope: 后端接口、数据库和企业微信审批引擎实现。 diff --git a/openspec/changes/update-employee-collection-approval-recovery/specs/employee-collection/spec.md b/openspec/changes/update-employee-collection-approval-recovery/specs/employee-collection/spec.md new file mode 100644 index 0000000..650603b --- /dev/null +++ b/openspec/changes/update-employee-collection-approval-recovery/specs/employee-collection/spec.md @@ -0,0 +1,12 @@ +## ADDED Requirements + +### Requirement: Recover Employee Collection Approval + +The frontend SHALL expose recovery for an employee collection application only when the backend returns `recoverable = true` and the user has the recovery permission. + +#### Scenario: Recover approval from application detail + +- **GIVEN** a核销申请返回 `recoverable = true` +- **WHEN** the user confirms “恢复审批” +- **THEN** the frontend MUST call `POST /api/admin/employee-collection-applications/{id}/recover-approval` +- **AND** MUST reload the application data after success or conflict diff --git a/openspec/changes/update-employee-collection-approval-recovery/tasks.md b/openspec/changes/update-employee-collection-approval-recovery/tasks.md new file mode 100644 index 0000000..27aefad --- /dev/null +++ b/openspec/changes/update-employee-collection-approval-recovery/tasks.md @@ -0,0 +1,12 @@ +## 1. Implementation + +- [x] 1.1 扩展核销申请类型与恢复接口响应。 +- [x] 1.2 增加恢复审批 API service 和唯一权限编码。 +- [x] 1.3 列表增加提交状态字段和恢复审批操作。 +- [x] 1.4 详情增加提交状态字段和恢复审批操作。 +- [x] 1.5 更新 `docs/权限.md`。 + +## 2. Verification + +- [x] 2.1 运行类型检查、lint、编码检查和构建。 +- [x] 2.2 运行 OpenSpec 严格校验。 diff --git a/src/api/modules/businessUserGroup.ts b/src/api/modules/businessUserGroup.ts index a361495..a8e24ca 100644 --- a/src/api/modules/businessUserGroup.ts +++ b/src/api/modules/businessUserGroup.ts @@ -6,6 +6,9 @@ import { BaseService } from '../BaseService' import type { BusinessUserGroupItem, BusinessUserGroupMembersResult, + BusinessUserGroupMembersPageResult, + BusinessUserGroupMembersQueryParams, + BusinessUserGroupMemberCandidatesQueryParams, BusinessUserGroupQueryParams, CreateBusinessUserGroupParams, DeleteBusinessUserGroupParams, @@ -67,25 +70,47 @@ export class BusinessUserGroupService extends BaseService { * 批量设置平台用户业务用户组归属(整体替换) * PUT /api/admin/business-user-groups/{id}/members */ - static setGroupMembers( + static addGroupMembers( id: number, data: SetBusinessUserGroupMembersParams ): Promise> { - return this.put>( + return this.post>( `/api/admin/business-user-groups/${id}/members`, data ) } + static getMemberCandidates( + id: number, + params?: BusinessUserGroupMemberCandidatesQueryParams + ): Promise> { + return this.get>( + `/api/admin/business-user-groups/${id}/member-candidates`, + params + ) + } + + /** 查询业务用户组现有成员 */ + static getGroupMembers( + id: number, + params?: BusinessUserGroupMembersQueryParams + ): Promise> { + return this.get>( + `/api/admin/business-user-groups/${id}/members`, + params + ) + } + /** * 批量清空平台用户业务用户组归属(DELETE 带 body) * DELETE /api/admin/business-user-groups/members */ - static clearGroupMembers( + static removeGroupMembers( + id: number, data: SetBusinessUserGroupMembersParams ): Promise> { return this.delete>( - '/api/admin/business-user-groups/members', + `/api/admin/business-user-groups/${id}/members`, undefined, { data } ) diff --git a/src/api/modules/employeeCollection.ts b/src/api/modules/employeeCollection.ts index 01b53e6..87053e9 100644 --- a/src/api/modules/employeeCollection.ts +++ b/src/api/modules/employeeCollection.ts @@ -20,7 +20,9 @@ import type { EmployeeCollectionPaymentMethodListResponse, EmployeeCollectionPaymentMethodQueryParams, EmployeeCollectionPaymentMethodResponse, - UpdateEmployeeCollectionPaymentMethodRequest + UpdateEmployeeCollectionPaymentMethodRequest, + BaseResponse, + RecoverEmployeeCollectionApplicationResponse } from '@/types/api' const PAYMENT_METHODS_BASE_URL = '/api/admin/employee-collection-payment-methods' @@ -146,4 +148,13 @@ export class EmployeeCollectionService extends BaseService { data ) } + + /** 恢复核销申请原审批提交 */ + static recoverApplicationApproval( + id: number + ): Promise> { + return this.post>( + `${APPLICATIONS_BASE_URL}/${id}/recover-approval` + ) + } } diff --git a/src/components/business/CreateRefundDialog.vue b/src/components/business/CreateRefundDialog.vue index a681f27..ac5ec98 100644 --- a/src/components/business/CreateRefundDialog.vue +++ b/src/components/business/CreateRefundDialog.vue @@ -23,8 +23,20 @@ + + + + + + - + + {{ opt.label }} @@ -84,7 +96,7 @@ @change="formRef?.validateField('refund_voucher_key')" />
- 客户收款信息方式下须同时提供客户收款信息与至少 1 个退款凭证 + 线下退款方式下须同时提供收款信息与至少 1 个退款凭证
@@ -112,7 +124,6 @@ import { RefundMethod, type CreateRefundRequest, type Order } from '@/types/api' import { RefundMethodOptions } from '@/config/constants/refund' import { formatMoney, yuanToFen } from '@/utils/business/format' - import { getErrorMessage } from '@/utils/business' import VoucherUpload from '@/components/business/VoucherUpload.vue' interface Props { @@ -222,6 +233,35 @@ return formatMoney(selectedOrder?.actual_paid_amount ?? 0, false) }) + const refundMethodLabel = computed( + () => RefundMethodOptions.find((option) => option.value === formData.method)?.label || '-' + ) + + const selectedOrder = computed(() => + orderSearchOptions.value.find((order) => order.id === formData.order_id) + ) + const paymentMethodLabel = computed(() => { + const labels = { + wallet: '钱包支付', + wechat: '微信支付', + alipay: '支付宝支付', + offline: '线下支付' + } + return selectedOrder.value ? labels[selectedOrder.value.payment_method] : '-' + }) + const buyerTypeLabel = computed(() => + selectedOrder.value?.buyer_type === 'agent' ? '代理商' : selectedOrder.value ? '个人客户' : '-' + ) + const walletRefundFixed = computed(() => selectedOrder.value?.payment_method === 'wallet') + + const resolveRefundMethod = (order: Order): RefundMethod => { + if (order.payment_method === 'wallet') { + return order.buyer_type === 'agent' ? RefundMethod.AGENT_WALLET : RefundMethod.ASSET_WALLET + } + if (order.payment_method === 'offline') return RefundMethod.CUSTOMER_ACCOUNT + return RefundMethod.ORIGINAL_ROUTE + } + const searchOrders = async (query: string) => { orderSearchLoading.value = true try { @@ -238,6 +278,11 @@ orderSearchOptions.value = res.data.items || [] if (hasInitialOrder.value && props.initialOrderId) { formData.order_id = props.initialOrderId + const initialOrder = orderSearchOptions.value.find((o) => o.id === props.initialOrderId) + if (initialOrder) { + formData.actual_received_amount = initialOrder.actual_paid_amount ?? 0 + formData.method = resolveRefundMethod(initialOrder) + } } if (hasInitialOrder.value && props.initialOrderNo) { const matchedOrder = orderSearchOptions.value.find( @@ -246,6 +291,7 @@ if (matchedOrder) { formData.order_id = matchedOrder.id formData.actual_received_amount = matchedOrder.actual_paid_amount ?? 0 + formData.method = resolveRefundMethod(matchedOrder) } } } @@ -264,6 +310,8 @@ const selectedOrder = orderSearchOptions.value.find((o) => o.id === orderId) if (selectedOrder) { formData.actual_received_amount = selectedOrder.actual_paid_amount ?? 0 + formData.method = resolveRefundMethod(selectedOrder) + formRef.value?.validateField('method') } } diff --git a/src/config/constants/augustIteration.ts b/src/config/constants/augustIteration.ts index 4a679de..5498fe2 100644 --- a/src/config/constants/augustIteration.ts +++ b/src/config/constants/augustIteration.ts @@ -12,6 +12,7 @@ export const AUGUST_PERMISSIONS = { applicationDetail: 'employee_collection:application_detail', applicationUpdate: 'employee_collection:application_update', applicationViewVoucher: 'employee_collection:application_view_voucher', + applicationRecoverApproval: 'employee_collection:application_recover_approval', paymentMethodPage: 'employee_collection:payment_method_view', paymentMethodCreate: 'employee_collection:payment_method_create', paymentMethodEdit: 'employee_collection:payment_method_edit', diff --git a/src/config/constants/refund.ts b/src/config/constants/refund.ts index 8a8d311..04215b0 100644 --- a/src/config/constants/refund.ts +++ b/src/config/constants/refund.ts @@ -20,17 +20,15 @@ export const RefundStatusNameMap: Record = { } /** 退款状态标签类型映射 */ -export const RefundStatusTypeMap: Record< - RefundStatus, - 'warning' | 'success' | 'danger' | 'info' -> = { - [RefundStatus.PENDING]: 'warning', - [RefundStatus.APPROVED]: 'success', - [RefundStatus.REJECTED]: 'danger', - [RefundStatus.RETURNED]: 'info', - [RefundStatus.REFUNDING]: 'warning', - [RefundStatus.REFUND_FAILED]: 'danger' -} +export const RefundStatusTypeMap: Record = + { + [RefundStatus.PENDING]: 'warning', + [RefundStatus.APPROVED]: 'success', + [RefundStatus.REJECTED]: 'danger', + [RefundStatus.RETURNED]: 'info', + [RefundStatus.REFUNDING]: 'warning', + [RefundStatus.REFUND_FAILED]: 'danger' + } /** 退款方式名称映射 */ export const RefundMethodNameMap: Record = { @@ -62,7 +60,7 @@ export const RefundFailureReasonNameMap: Record = { /** 退款方式选项(表单用) */ export const RefundMethodOptions = [ { label: '原路退回', value: RefundMethod.ORIGINAL_ROUTE }, - { label: '客户收款信息', value: RefundMethod.CUSTOMER_ACCOUNT }, + { label: '线下退款', value: RefundMethod.CUSTOMER_ACCOUNT }, { label: '资产钱包', value: RefundMethod.ASSET_WALLET }, { label: '代理钱包', value: RefundMethod.AGENT_WALLET } ] @@ -86,4 +84,4 @@ export const canResubmitRefund = (item: { export const canManualApproveRefund = (item: { status?: RefundStatus approval_instance_id?: number | null -}): boolean => item.status === RefundStatus.PENDING && !item.approval_instance_id \ No newline at end of file +}): boolean => item.status === RefundStatus.PENDING && !item.approval_instance_id diff --git a/src/types/api/businessUserGroup.ts b/src/types/api/businessUserGroup.ts index d247e7a..f422ceb 100644 --- a/src/types/api/businessUserGroup.ts +++ b/src/types/api/businessUserGroup.ts @@ -19,6 +19,7 @@ export interface BusinessUserGroupItem { remark: string // 备注 created_at: string // 创建时间 updated_at: string // 更新时间 + member_count?: number } /** 业务用户组列表查询参数 */ @@ -67,8 +68,49 @@ export interface SetBusinessUserGroupMembersParams { account_ids: number[] // 平台用户账号ID列表,至少一个;每个账号必须是启用平台用户 } +export interface BusinessUserGroupMembersQueryParams extends PaginationParams { + keyword?: string + account_status?: 'enabled' | 'disabled' | 'deleted' +} + +export type BusinessUserGroupMemberScope = 'all' | 'ungrouped' | 'current' | 'other' + +export interface BusinessUserGroupMemberCandidatesQueryParams extends PaginationParams { + keyword?: string + scope?: BusinessUserGroupMemberScope +} + +export interface BusinessUserGroupMember { + account_id: number + username: string + phone?: string | null + enabled?: boolean + deleted?: boolean + status_name?: string | null + member_created_at?: string | null + current_group_id?: number | null + current_group_code?: string | null + current_group_name?: string | null + operation?: 'add' | 'already_member' | 'move' | string + operation_name?: string | null +} + +export interface BusinessUserGroupMemberCandidate extends BusinessUserGroupMember { + id?: number + phone_summary?: string | null + action?: 'add' | 'already_member' | 'move' | string + can_add?: boolean +} + +export interface BusinessUserGroupMembersPageResult { + items: BusinessUserGroupMember[] | null + page?: number + size?: number + total?: number +} + /** 批量设置/清空成员结果 */ export interface BusinessUserGroupMembersResult { group_id: number // 目标业务用户组ID;清空操作为 0 account_ids: number[] | null // 本次成功维护归属的平台用户账号ID列表 -} \ No newline at end of file +} diff --git a/src/types/api/employeeCollection.ts b/src/types/api/employeeCollection.ts index aa19046..9dd9520 100644 --- a/src/types/api/employeeCollection.ts +++ b/src/types/api/employeeCollection.ts @@ -229,6 +229,12 @@ export interface EmployeeCollectionApplication { decided_at?: string | null latest_approval_instance_id?: number | null latest_attempt_id?: number | null + approval_failure_summary?: string | null + approval_submission_name?: string | null + approval_submission_status?: number | null + last_recovery_at?: string | null + recoverable?: boolean + recovery_result?: string | null created_at?: string | null updated_at?: string | null } @@ -279,6 +285,17 @@ export interface EmployeeCollectionApplicationSubmitResult { attempt?: EmployeeCollectionApplicationAttempt | null } +export interface RecoverEmployeeCollectionApplicationResponse { + action?: string + application_id?: number + approval_instance_id?: number | null + last_recovery_at?: string | null + recoverable?: boolean + result?: string + submission_status?: number | null + submission_status_name?: string | null +} + // ========== 响应类型 ========== export type EmployeeCollectionPaymentMethodListResponse = BaseResponse< diff --git a/src/views/finance/employee-collection/applications/detail.vue b/src/views/finance/employee-collection/applications/detail.vue index 99fce01..0e9123f 100644 --- a/src/views/finance/employee-collection/applications/detail.vue +++ b/src/views/finance/employee-collection/applications/detail.vue @@ -9,6 +9,17 @@ 返回

核销申请详情

+ + 恢复审批 + @@ -112,6 +123,7 @@ ElCard, ElIcon, ElMessage, + ElMessageBox, ElTable, ElTableColumn, ElTimeline, @@ -146,6 +158,7 @@ const { hasAuth } = useAuth() const loading = ref(false) + const recovering = ref(false) const detail = ref(null) const application = computed( @@ -186,6 +199,20 @@ prop: 'status', formatter: (value, data) => data.status_name || getApplicationStatusLabel(value) }, + { + label: '审批提交状态', + formatter: (_, data) => data.approval_submission_name || '-' + }, + { + label: '提交失败摘要', + formatter: (_, data) => data.approval_failure_summary || '-', + fullWidth: true + }, + { + label: '最近恢复时间', + formatter: (_, data) => + data.last_recovery_at ? formatDateTime(data.last_recovery_at) : '-' + }, { label: '审批实例ID', prop: 'latest_approval_instance_id', @@ -241,6 +268,38 @@ router.push({ path: RoutesAlias.EmployeeCollectionApplications }) } + const recoverApproval = async () => { + if ( + !application.value?.recoverable || + !hasAuth(AUGUST_PERMISSIONS.employeeCollection.applicationRecoverApproval) || + recovering.value + ) { + return + } + try { + await ElMessageBox.confirm('确定要恢复该核销申请的原审批提交吗?', '恢复审批', { + type: 'warning', + confirmButtonText: '确定', + cancelButtonText: '取消' + }) + } catch { + return + } + recovering.value = true + try { + const res = await EmployeeCollectionService.recoverApplicationApproval(application.value.id) + ElMessage[res.code === 0 ? 'success' : 'error']( + res.msg || (res.code === 0 ? '恢复审批成功' : '恢复审批失败') + ) + await loadDetail() + } catch (error) { + ElMessage.error(getErrorMessage(error, '恢复审批失败')) + await loadDetail() + } finally { + recovering.value = false + } + } + onMounted(() => void loadDetail()) diff --git a/src/views/finance/employee-collection/applications/index.vue b/src/views/finance/employee-collection/applications/index.vue index 665fe34..c2b1129 100644 --- a/src/views/finance/employee-collection/applications/index.vue +++ b/src/views/finance/employee-collection/applications/index.vue @@ -58,7 +58,7 @@