feat: 角色默认信用与店铺实际额度管理
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m12s

This commit is contained in:
luo
2026-07-23 14:10:48 +08:00
parent d5fd8ac564
commit d7c2c146fe
14 changed files with 669 additions and 21 deletions

View File

@@ -0,0 +1,33 @@
## Context
信用额度分为两个层级:客户角色上的默认信用只决定未来新建代理店铺的初始值,店铺上的实际信用额度才参与资金页展示和后续额度调整。两者必须解耦,避免角色配置变更影响已有店铺。
## Goals / Non-Goals
Goals:
- 明确角色默认信用和店铺实际信用额度的职责边界。
- 明确资金概况页读取后端返回的金额和版本作为展示、调额和并发控制来源。
- 明确关闭信用时额度输入归零。
Non-Goals:
- 不在前端重新计算可用金额、欠款金额或是否欠款。
- 不为平台员工角色提供信用配置。
- 不批量同步历史店铺额度。
## Decisions
- 角色默认信用保存到 `PUT /api/admin/roles/{id}/default-credit`,请求体为 `credit_enabled``credit_limit`
- 店铺实际信用额度保存到 `PUT /api/admin/shops/{id}/credit-limit`,请求体为 `credit_enabled``credit_limit``version`
- 资金概况继续通过 `GET /api/admin/shops/fund-summary` 获取,前端展示 `balance``frozen_balance``credit_enabled``credit_limit``available_balance``is_in_debt``debt_amount``version`
- 调整弹框展示修改前后金额预览,但最终可用金额、欠款金额和欠款状态仍以后端刷新后的资金概况为准。
## Risks / Trade-offs
- 并发更新可能覆盖他人修改。通过提交 `version` 并在冲突时刷新资金概况降低风险。
- 用户可能误以为角色默认信用会修改已有店铺。通过固定提示“修改后不会影响已有店铺”降低误解。
## Open Questions
- 并发冲突的后端错误码或 HTTP 状态码是否固定为某个值,需要实现时与接口文档对齐。

View File

@@ -0,0 +1,30 @@
# Change: 角色默认信用与店铺实际额度管理
## Why
当前角色信用配置容易被理解为会直接影响已有店铺,且代理商资金页缺少单独维护店铺实际信用额度的入口。需要将“未来新建店铺的默认信用”和“已有店铺的实际信用额度”拆分管理,避免修改角色配置误伤存量店铺。
## What Changes
- 客户角色配置页新增“新建代理默认信用”开关和额度输入,并提示“修改后不会影响已有店铺”。
- 平台员工角色不展示默认信用配置。
- 店铺创建时使用所选客户角色的默认信用初始化新店铺实际信用额度。
- 代理商资金概况页展示现金余额、冻结金额、实际信用额度、可用金额、欠款金额和版本。
- 代理商资金概况页提供独立的店铺实际额度调整弹框,展示修改前后金额预览。
- 调整店铺实际额度时携带资金概况版本,遇到并发冲突后提示用户并刷新最新资金概况。
- 前端不重新计算可用金额、欠款金额或资金状态,金额展示以接口返回为准。
## Impact
- Affected specs: `role-management`, `shop-management`, `commission-management`
- Affected code:
- `src/api/modules/role.ts`
- `src/api/modules/shop.ts`
- `src/api/modules/commission.ts`
- `src/types/api/role.ts`
- `src/types/api/shop.ts`
- `src/types/api/commission.ts`
- `src/views/system/role/index.vue`
- `src/views/shop-management/list/index.vue`
- `src/views/commission-management/agent-fund-overview/index.vue`
- Source product note: `docs/产品迭代7月份/15- 角色默认信用与店铺实际额度管理.md`

View File

@@ -0,0 +1,84 @@
## ADDED Requirements
### Requirement: Agent Fund Summary Actual Credit Display
The agent fund overview page SHALL display shop actual credit and fund summary fields from `GET /api/admin/shops/fund-summary` without recalculating monetary values on the frontend.
#### Scenario: Display fund summary credit fields
- **GIVEN** 用户进入代理商资金概况页
- **WHEN** `GET /api/admin/shops/fund-summary` 返回资金概况记录
- **THEN** 页面 MUST 展示现金余额 `balance`
- **AND** 页面 MUST 展示冻结金额 `frozen_balance`
- **AND** 页面 MUST 展示实际信用额度 `credit_limit`
- **AND** 页面 MUST 展示可用金额 `available_balance`
- **AND** 页面 MUST 展示欠款金额 `debt_amount`
- **AND** 页面 MUST 展示版本 `version`
#### Scenario: Display credit disabled state
- **GIVEN** 资金概况记录返回 `credit_enabled=false`
- **WHEN** 页面渲染该记录
- **THEN** 页面 MUST 将实际信用显示为关闭状态
- **AND** 页面 MUST display credit limit as `0` or backend-provided formatted value
#### Scenario: Do not recalculate fund amounts on frontend
- **GIVEN** 资金概况接口返回 `available_balance``is_in_debt``debt_amount`
- **WHEN** 页面渲染资金概况
- **THEN** 前端 MUST 使用接口返回的 `available_balance`
- **AND** 前端 MUST 使用接口返回的 `is_in_debt`
- **AND** 前端 MUST 使用接口返回的 `debt_amount`
- **AND** 前端 MUST NOT 根据现金余额、冻结金额或信用额度重新计算这些字段
### Requirement: Shop Actual Credit Adjustment Dialog
The agent fund overview page SHALL provide an independent dialog for adjusting a shop's actual credit limit and previewing the before/after amount.
#### Scenario: Open actual credit adjustment dialog
- **GIVEN** 用户正在代理商资金概况页查看店铺资金记录
- **WHEN** 用户点击调整实际信用额度入口
- **THEN** 页面 MUST 打开独立调整弹框
- **AND** 弹框 MUST 展示修改前信用启用状态和信用额度
- **AND** 弹框 MUST 展示修改后信用启用状态和信用额度预览
#### Scenario: Submit actual credit adjustment
- **GIVEN** 用户已经在调整弹框内修改实际信用配置
- **WHEN** 用户确认提交
- **THEN** 前端 MUST submit `credit_enabled``credit_limit` and `version` to `PUT /api/admin/shops/{id}/credit-limit`
- **AND** 保存成功后页面 MUST 关闭弹框
- **AND** 页面 MUST 刷新代理商资金概况列表
#### Scenario: Disable credit in adjustment dialog
- **GIVEN** 用户正在调整弹框内修改实际信用配置
- **WHEN** 用户关闭信用开关
- **THEN** 弹框 MUST 将额度输入归零
- **AND** 修改后金额预览 MUST reflect disabled credit with zero credit limit
#### Scenario: Handle concurrent credit update conflict
- **GIVEN** 用户打开调整弹框时记录了资金概况 `version`
- **AND** 该店铺资金概况已被其他操作更新
- **WHEN** 用户提交旧版本的实际信用调整
- **THEN** 页面 MUST 展示并发冲突提示
- **AND** 页面 MUST 刷新最新代理商资金概况
- **AND** 页面 MUST NOT keep showing stale version as current data
### Requirement: Agent Fund Summary Credit Response Contract
The frontend SHALL read actual credit fields from `GET /api/admin/shops/fund-summary` response records.
#### Scenario: Read actual credit response fields
- **WHEN** 前端请求 `GET /api/admin/shops/fund-summary`
- **THEN** 前端 MUST read `balance`
- **AND** 前端 MUST read `frozen_balance`
- **AND** 前端 MUST read `credit_enabled`
- **AND** 前端 MUST read `credit_limit`
- **AND** 前端 MUST read `available_balance`
- **AND** 前端 MUST read `is_in_debt`
- **AND** 前端 MUST read `debt_amount`
- **AND** 前端 MUST read `version`

View File

@@ -0,0 +1,43 @@
## ADDED Requirements
### Requirement: Customer Role Default Credit Configuration
The role management page SHALL allow customer roles to configure default credit only for future newly created agent shops.
#### Scenario: Display default credit controls for customer roles
- **GIVEN** 用户正在客户角色配置页编辑客户角色
- **WHEN** 页面渲染角色表单或配置区域
- **THEN** 页面 MUST 展示“新建代理默认信用”开关
- **AND** 页面 MUST 展示信用额度输入框
- **AND** 页面 MUST 展示提示文案“修改后不会影响已有店铺”
#### Scenario: Hide default credit controls for platform employee roles
- **GIVEN** 用户正在编辑平台员工角色
- **WHEN** 页面渲染角色表单或配置区域
- **THEN** 页面 MUST NOT 展示“新建代理默认信用”开关
- **AND** 页面 MUST NOT 展示信用额度输入框
#### Scenario: Save customer role default credit
- **GIVEN** 用户正在编辑客户角色默认信用
- **WHEN** 用户提交默认信用配置
- **THEN** 前端 MUST call `PUT /api/admin/roles/{id}/default-credit`
- **AND** 请求体 MUST include `credit_enabled: bool` and `credit_limit: int64`
- **AND** 保存成功后页面 MUST 提示保存成功
#### Scenario: Disable customer role default credit
- **GIVEN** 用户正在编辑客户角色默认信用
- **WHEN** 用户关闭“新建代理默认信用”开关
- **THEN** 页面 MUST 将额度输入归零
- **AND** 提交时请求体 MUST include `credit_enabled=false`
- **AND** 提交时请求体 MUST include `credit_limit=0`
#### Scenario: Role default credit does not mutate existing shops
- **GIVEN** 已有店铺已经创建并拥有实际信用额度
- **WHEN** 用户修改该店铺所属客户角色的默认信用配置
- **THEN** 系统 MUST NOT 修改已有店铺的实际信用额度
- **AND** 已有店铺的实际额度 MUST 只能通过店铺实际额度调整入口修改

View File

@@ -0,0 +1,48 @@
## ADDED Requirements
### Requirement: New Shop Credit Initialization From Customer Role
The shop creation flow SHALL initialize a newly created agent shop's actual credit from the selected customer role's default credit configuration.
#### Scenario: Create agent shop with enabled role default credit
- **GIVEN** 选中的客户角色已启用默认信用
- **AND** 该客户角色配置了默认信用额度
- **WHEN** 用户创建新的代理店铺
- **THEN** 新店铺的实际信用 MUST 初始化为启用状态
- **AND** 新店铺的实际信用额度 MUST 使用该客户角色的默认信用额度
#### Scenario: Create agent shop with disabled role default credit
- **GIVEN** 选中的客户角色未启用默认信用
- **WHEN** 用户创建新的代理店铺
- **THEN** 新店铺的实际信用 MUST 初始化为关闭状态
- **AND** 新店铺的实际信用额度 MUST be `0`
#### Scenario: Existing shop keeps actual credit after role default change
- **GIVEN** 店铺已经完成创建
- **WHEN** 用户修改该店铺所属客户角色的默认信用配置
- **THEN** 该店铺的实际信用启用状态 MUST NOT change
- **AND** 该店铺的实际信用额度 MUST NOT change
### Requirement: Shop Actual Credit Limit Update API
The frontend SHALL update an existing shop's actual credit limit through `PUT /api/admin/shops/{id}/credit-limit` with optimistic concurrency versioning.
#### Scenario: Submit shop actual credit update
- **GIVEN** 用户正在调整已有店铺实际信用额度
- **WHEN** 用户提交调整
- **THEN** 前端 MUST call `PUT /api/admin/shops/{id}/credit-limit`
- **AND** 请求体 MUST include `credit_enabled: bool`
- **AND** 请求体 MUST include `credit_limit: int64`
- **AND** 请求体 MUST include `version: int64`
#### Scenario: Disable shop actual credit
- **GIVEN** 用户正在调整已有店铺实际信用额度
- **WHEN** 用户关闭信用开关
- **THEN** 页面 MUST 将额度输入归零
- **AND** 提交时请求体 MUST include `credit_enabled=false`
- **AND** 提交时请求体 MUST include `credit_limit=0`

View File

@@ -0,0 +1,34 @@
## 1. API And Types
- [x] 1.1 在角色 API 和类型中新增 `PUT /api/admin/roles/{id}/default-credit` 请求类型与服务方法。
- [x] 1.2 在店铺 API 和类型中新增 `PUT /api/admin/shops/{id}/credit-limit` 请求类型与服务方法。
- [x] 1.3 扩展代理商资金概况类型,包含 `credit_enabled``credit_limit``available_balance``is_in_debt``debt_amount``version`
## 2. Role Default Credit
- [x] 2.1 在客户角色配置页展示“新建代理默认信用”开关、额度输入和“修改后不会影响已有店铺”提示。
- [x] 2.2 平台员工角色不展示信用配置。
- [x] 2.3 关闭默认信用时将额度输入归零并提交 `credit_limit=0`
- [x] 2.4 保存默认信用配置时调用 `PUT /api/admin/roles/{id}/default-credit`
## 3. Shop Credit Initialization
- [x] 3.1 确认店铺创建页不把角色默认信用表现为会影响已有店铺的配置。
- [x] 3.2 确认新建店铺实际信用额度由后端按所选客户角色默认信用初始化。
## 4. Agent Fund Overview
- [x] 4.1 在代理商资金概况页展示现金余额、冻结金额、实际信用额度、可用金额、欠款金额和版本。
- [x] 4.2 金额字段按接口返回值展示,不在前端重新计算可用金额或欠款金额。
- [x] 4.3 新增店铺实际额度调整弹框,展示当前值、修改后值和关闭信用归零行为。
- [x] 4.4 提交调额时携带当前 `version` 调用 `PUT /api/admin/shops/{id}/credit-limit`
- [x] 4.5 并发冲突时展示提示并刷新最新资金概况。
## 5. Verification
- [ ] 5.1 验证客户角色默认信用保存后不会修改已有店铺展示的实际信用额度。
- [ ] 5.2 验证新建店铺使用所选客户角色默认信用初始化实际额度。
- [ ] 5.3 验证资金概况页展示字段与接口返回一致。
- [ ] 5.4 验证关闭信用时额度输入归零。
- [ ] 5.5 验证并发冲突后页面提示并刷新最新资金概况。
- [x] 5.6 运行相关 lint、类型检查或构建命令。

View File

@@ -8,7 +8,7 @@ import type {
PlatformRole, PlatformRole,
RoleQueryParams, RoleQueryParams,
PlatformRoleFormData, PlatformRoleFormData,
PermissionTreeNode, UpdateRoleDefaultCreditRequest,
BaseResponse, BaseResponse,
PaginationResponse PaginationResponse
} from '@/types/api' } from '@/types/api'
@@ -51,6 +51,19 @@ export class RoleService extends BaseService {
return this.update(`/api/admin/roles/${id}`, data) return this.update(`/api/admin/roles/${id}`, data)
} }
/**
* 更新客户角色默认信用
* PUT /api/admin/roles/{id}/default-credit
* @param id 角色ID
* @param data 默认信用配置
*/
static updateRoleDefaultCredit(
id: number,
data: UpdateRoleDefaultCreditRequest
): Promise<BaseResponse> {
return this.put<BaseResponse>(`/api/admin/roles/${id}/default-credit`, data)
}
/** /**
* 删除角色 * 删除角色
* DELETE /api/admin/roles/{id} * DELETE /api/admin/roles/{id}

View File

@@ -10,6 +10,7 @@ import type {
UpdateShopParams, UpdateShopParams,
ShopRolesResponse, ShopRolesResponse,
AssignShopRolesRequest, AssignShopRolesRequest,
UpdateShopCreditLimitRequest,
BaseResponse, BaseResponse,
PaginationResponse PaginationResponse
} from '@/types/api' } from '@/types/api'
@@ -114,4 +115,17 @@ export class ShopService extends BaseService {
static deleteShopRole(shopId: number, roleId: number): Promise<BaseResponse> { static deleteShopRole(shopId: number, roleId: number): Promise<BaseResponse> {
return this.delete<BaseResponse>(`/api/admin/shops/${shopId}/roles/${roleId}`) return this.delete<BaseResponse>(`/api/admin/shops/${shopId}/roles/${roleId}`)
} }
/**
* 更新店铺实际信用额度
* PUT /api/admin/shops/{id}/credit-limit
* @param shopId 店铺ID
* @param data 实际信用额度配置
*/
static updateShopCreditLimit(
shopId: number,
data: UpdateShopCreditLimitRequest
): Promise<BaseResponse> {
return this.put<BaseResponse>(`/api/admin/shops/${shopId}/credit-limit`, data)
}
} }

View File

@@ -215,6 +215,12 @@ export interface ShopFundSummaryItem {
frozen_balance: number // 现金冻结金额(分) frozen_balance: number // 现金冻结金额(分)
cash_available: number // 现金可用余额(分) cash_available: number // 现金可用余额(分)
low_balance_warning: boolean // 现金可用余额不足100元预警 low_balance_warning: boolean // 现金可用余额不足100元预警
credit_enabled: boolean // 实际信用额度是否启用
credit_limit: number // 实际信用额度(分)
available_balance: number // 可用金额(分),以后端返回为准
is_in_debt: boolean // 是否欠款,以后端返回为准
debt_amount: number // 欠款金额(分),以后端返回为准
version: number // 资金概况版本,用于并发控制
total_commission: number // 总佣金(分) total_commission: number // 总佣金(分)
available_commission: number // 可提现佣金(分) available_commission: number // 可提现佣金(分)
frozen_commission: number // 冻结中佣金(分) frozen_commission: number // 冻结中佣金(分)

View File

@@ -28,6 +28,8 @@ export interface PlatformRole {
role_desc: string // 角色描述 role_desc: string // 角色描述
role_type: RoleType // 角色类型 role_type: RoleType // 角色类型
status: RoleStatus // 状态 status: RoleStatus // 状态
credit_enabled?: boolean // 新建代理默认信用是否启用(仅客户角色)
credit_limit?: number // 新建代理默认信用额度(分,仅客户角色)
} }
// 角色查询参数 // 角色查询参数
@@ -44,3 +46,9 @@ export interface PlatformRoleFormData {
role_type: RoleType role_type: RoleType
status: RoleStatus status: RoleStatus
} }
// 更新客户角色默认信用参数
export interface UpdateRoleDefaultCreditRequest {
credit_enabled: boolean
credit_limit: number
}

View File

@@ -101,3 +101,10 @@ export interface ShopRolesResponse {
export interface AssignShopRolesRequest { export interface AssignShopRolesRequest {
role_ids: number[] | null // 角色ID列表 role_ids: number[] | null // 角色ID列表
} }
// 更新店铺实际信用额度请求
export interface UpdateShopCreditLimitRequest {
credit_enabled: boolean // 是否启用实际信用额度
credit_limit: number // 实际信用额度(分)
version: number // 资金概况版本,用于并发控制
}

View File

@@ -87,10 +87,8 @@ declare module 'vue' {
ElAlert: typeof import('element-plus/es')['ElAlert'] ElAlert: typeof import('element-plus/es')['ElAlert']
ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCalendar: typeof import('element-plus/es')['ElCalendar']
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCascader: typeof import('element-plus/es')['ElCascader'] ElCascader: typeof import('element-plus/es')['ElCascader']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
@@ -113,9 +111,7 @@ declare module 'vue' {
ElOption: typeof import('element-plus/es')['ElOption'] ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination'] ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover'] ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow'] ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
@@ -127,8 +123,6 @@ declare module 'vue' {
ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs'] ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag'] 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'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
ElUpload: typeof import('element-plus/es')['ElUpload'] ElUpload: typeof import('element-plus/es')['ElUpload']

View File

@@ -32,7 +32,7 @@
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:actions="getActions" :actions="getActions"
:actionsWidth="120" :actionsWidth="180"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
> >
@@ -416,13 +416,74 @@
</div> </div>
</template> </template>
</ElDialog> </ElDialog>
<!-- 店铺实际信用额度调整弹框 -->
<ElDialog
v-model="creditDialogVisible"
:title="`调整实际信用额度 - ${currentCreditShop?.shop_name || ''}`"
width="520px"
@closed="resetCreditDialog"
>
<ElDescriptions :column="1" border class="credit-preview">
<ElDescriptionsItem label="店铺名称">
{{ currentCreditShop?.shop_name || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="当前版本">
{{ currentCreditShop?.version ?? '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="修改前">
{{
formatCreditPreview(currentCreditShop?.credit_enabled, currentCreditShop?.credit_limit)
}}
</ElDescriptionsItem>
<ElDescriptionsItem label="修改后">
{{ formatCreditPreview(creditForm.credit_enabled, creditFormCreditLimitFen) }}
</ElDescriptionsItem>
</ElDescriptions>
<ElAlert
title="实际可用金额、欠款金额和欠款状态以后端刷新后的资金概况为准。"
type="info"
:closable="false"
show-icon
class="credit-dialog-alert"
/>
<ElForm ref="creditFormRef" :model="creditForm" :rules="creditRules" label-width="110px">
<ElFormItem label="启用信用">
<ElSwitch v-model="creditForm.credit_enabled" @change="handleCreditEnabledChange" />
</ElFormItem>
<ElFormItem label="实际信用额度" prop="credit_limit_yuan">
<ElInputNumber
v-model="creditForm.credit_limit_yuan"
:disabled="!creditForm.credit_enabled"
:min="0"
:precision="2"
:step="100"
controls-position="right"
style="width: 100%"
placeholder="请输入实际信用额度"
/>
<div class="credit-dialog-tip">单位关闭信用时额度将自动归零</div>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="creditDialogVisible = false">取消</ElButton>
<ElButton type="primary" :loading="creditSubmitting" @click="handleCreditSubmit">
确认调整
</ElButton>
</div>
</template>
</ElDialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h, watch, onBeforeUnmount, ref, reactive, onMounted, computed } from 'vue' import { h, watch, onBeforeUnmount, ref, reactive, onMounted, computed, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { CommissionService } from '@/api/modules' import { CommissionService, ShopService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag } from 'element-plus' import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { MainWalletTransactionType } from '@/types/api/commission' import { MainWalletTransactionType } from '@/types/api/commission'
@@ -438,7 +499,7 @@
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { formatDateTime, formatMoney } from '@/utils/business/format' import { fenToYuan, formatDateTime, formatMoney, yuanToFen } from '@/utils/business/format'
import { import {
CommissionStatusMap, CommissionStatusMap,
WithdrawalStatusMap, WithdrawalStatusMap,
@@ -508,6 +569,20 @@
remark: '' remark: ''
}) })
// 实际信用额度调整弹框状态
const creditDialogVisible = ref(false)
const creditFormRef = ref<FormInstance>()
const creditSubmitting = ref(false)
const currentCreditShop = ref<ShopFundSummaryItem | null>(null)
const creditForm = reactive({
credit_enabled: false,
credit_limit_yuan: 0
})
const creditFormCreditLimitFen = computed(() =>
creditForm.credit_enabled ? yuanToFen(creditForm.credit_limit_yuan) || 0 : 0
)
// 佣金修正表单验证规则 // 佣金修正表单验证规则
const resolveRules = computed<FormRules>(() => ({ const resolveRules = computed<FormRules>(() => ({
amount: amount:
@@ -525,6 +600,36 @@
remark: [{ max: 500, message: '备注最多500字符', trigger: 'blur' }] remark: [{ max: 500, message: '备注最多500字符', trigger: 'blur' }]
})) }))
const creditRules = computed<FormRules>(() => ({
credit_limit_yuan: [
{
validator: (
_rule: unknown,
value: number | undefined,
callback: (error?: Error) => void
) => {
if (!creditForm.credit_enabled) {
callback()
return
}
if (value === undefined || value === null || Number.isNaN(value)) {
callback(new Error('请输入实际信用额度'))
return
}
if (value < 0) {
callback(new Error('实际信用额度不能小于0'))
return
}
callback()
},
trigger: 'blur'
}
]
}))
// 预充值钱包流水状态 // 预充值钱包流水状态
const mainWalletLoading = ref(false) const mainWalletLoading = ref(false)
const mainWalletTableRef = ref() const mainWalletTableRef = ref()
@@ -552,6 +657,10 @@
{ label: '手机号', prop: 'phone' }, { label: '手机号', prop: 'phone' },
{ label: '现金余额', prop: 'balance' }, { label: '现金余额', prop: 'balance' },
{ label: '冻结金额', prop: 'frozen_balance' }, { label: '冻结金额', prop: 'frozen_balance' },
{ label: '实际信用额度', prop: 'credit_limit' },
{ label: '可用金额', prop: 'available_balance' },
{ label: '欠款金额', prop: 'debt_amount' },
{ label: '版本', prop: 'version' },
{ label: '余额预警', prop: 'low_balance_warning' }, { label: '余额预警', prop: 'low_balance_warning' },
{ label: '总佣金', prop: 'total_commission' }, { label: '总佣金', prop: 'total_commission' },
{ label: '可提现', prop: 'available_commission' }, { label: '可提现', prop: 'available_commission' },
@@ -656,6 +765,51 @@
minWidth: 120, minWidth: 120,
formatter: (row: ShopFundSummaryItem) => formatMoney(row.frozen_balance) formatter: (row: ShopFundSummaryItem) => formatMoney(row.frozen_balance)
}, },
{
prop: 'credit_limit',
label: '实际信用额度',
minWidth: 150,
formatter: (row: ShopFundSummaryItem) => {
if (!row.credit_enabled) {
return h('span', { style: 'color: var(--el-text-color-secondary)' }, '未启用 / ¥0.00')
}
return h(
'span',
{ style: 'color: var(--el-color-warning); font-weight: 500' },
formatMoney(row.credit_limit)
)
}
},
{
prop: 'available_balance',
label: '可用金额',
minWidth: 130,
formatter: (row: ShopFundSummaryItem) => {
return h(
'span',
{ style: 'color: var(--el-color-success); font-weight: 500' },
formatMoney(row.available_balance)
)
}
},
{
prop: 'debt_amount',
label: '欠款金额',
minWidth: 130,
formatter: (row: ShopFundSummaryItem) => {
const amountText = formatMoney(row.debt_amount)
if (!row.is_in_debt) return amountText
return h('span', { style: 'color: var(--el-color-danger); font-weight: 500' }, amountText)
}
},
{
prop: 'version',
label: '版本',
minWidth: 90,
formatter: (row: ShopFundSummaryItem) => row.version ?? '-'
},
{ {
prop: 'low_balance_warning', prop: 'low_balance_warning',
label: '余额预警', label: '余额预警',
@@ -803,9 +957,105 @@
}) })
} }
actions.push({
label: '调整额度',
handler: () => showCreditDialog(row),
type: 'primary'
})
return actions return actions
} }
const formatCreditPreview = (enabled?: boolean, creditLimit?: number) => {
return enabled ? `启用 / ${formatMoney(creditLimit || 0)}` : '关闭 / ¥0.00'
}
const showCreditDialog = (row: ShopFundSummaryItem) => {
currentCreditShop.value = row
creditForm.credit_enabled = Boolean(row.credit_enabled)
creditForm.credit_limit_yuan = row.credit_enabled ? fenToYuan(row.credit_limit) : 0
creditDialogVisible.value = true
nextTick(() => {
creditFormRef.value?.clearValidate()
})
}
const handleCreditEnabledChange = (enabled: boolean | string | number) => {
if (!enabled) {
creditForm.credit_limit_yuan = 0
creditFormRef.value?.clearValidate('credit_limit_yuan')
}
}
const resetCreditDialog = () => {
creditFormRef.value?.resetFields()
currentCreditShop.value = null
creditForm.credit_enabled = false
creditForm.credit_limit_yuan = 0
}
const isCreditConflictMessage = (message?: string) => {
if (!message) return false
return /版本|冲突|并发|过期|conflict/i.test(message)
}
const isCreditConflictResponse = (response: any) => {
return response?.code === 409 || isCreditConflictMessage(response?.msg)
}
const isCreditConflictError = (error: any) => {
return (
error?.response?.status === 409 ||
error?.response?.data?.code === 409 ||
isCreditConflictMessage(error?.response?.data?.msg || error?.message)
)
}
const handleCreditConflict = async () => {
ElMessage.warning('资金概况已被更新,已刷新最新数据,请重新调整')
creditDialogVisible.value = false
await getTableData()
}
const handleCreditSubmit = async () => {
if (!creditFormRef.value || !currentCreditShop.value) return
await creditFormRef.value.validate(async (valid) => {
if (!valid || !currentCreditShop.value) return
creditSubmitting.value = true
try {
const res = await ShopService.updateShopCreditLimit(currentCreditShop.value.shop_id, {
credit_enabled: creditForm.credit_enabled,
credit_limit: creditFormCreditLimitFen.value,
version: currentCreditShop.value.version
})
if (res.code === 0) {
ElMessage.success('实际信用额度调整成功')
creditDialogVisible.value = false
await getTableData()
return
}
if (isCreditConflictResponse(res)) {
await handleCreditConflict()
return
}
ElMessage.error(res.msg || '实际信用额度调整失败')
} catch (error: any) {
if (isCreditConflictError(error)) {
await handleCreditConflict()
} else {
console.error('实际信用额度调整失败:', error)
}
} finally {
creditSubmitting.value = false
}
})
}
// 监听tab切换 // 监听tab切换
watch(activeTab, (newTab) => { watch(activeTab, (newTab) => {
if (newTab === 'commission') { if (newTab === 'commission') {
@@ -1084,6 +1334,21 @@
} }
} }
.credit-preview {
margin-bottom: 16px;
}
.credit-dialog-alert {
margin-bottom: 16px;
}
.credit-dialog-tip {
margin-top: 6px;
font-size: 12px;
line-height: 1.4;
color: var(--el-text-color-secondary);
}
@media (width <= 1200px) { @media (width <= 1200px) {
.main-wallet-filter__grid { .main-wallet-filter__grid {
grid-template-columns: repeat(2, minmax(220px, 1fr)); grid-template-columns: repeat(2, minmax(220px, 1fr));

View File

@@ -80,6 +80,27 @@
:inactive-value="CommonStatus.DISABLED" :inactive-value="CommonStatus.DISABLED"
/> />
</ElFormItem> </ElFormItem>
<template v-if="isCustomerRoleForm">
<ElFormItem label="新建代理默认信用">
<ElSwitch
v-model="form.credit_enabled"
@change="handleDefaultCreditEnabledChange"
/>
</ElFormItem>
<ElFormItem label="默认信用额度">
<ElInputNumber
v-model="form.credit_limit_yuan"
:disabled="!form.credit_enabled"
:min="0"
:precision="2"
:step="100"
controls-position="right"
style="width: 100%"
placeholder="请输入默认信用额度"
/>
<div class="credit-form-tip">单位修改后不会影响已有店铺</div>
</ElFormItem>
</template>
</ElForm> </ElForm>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
@@ -226,11 +247,12 @@
ElOption ElOption
} from 'element-plus' } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { RoleType } from '@/types/api'
import type { PlatformRole, PermissionTreeNode } from '@/types/api' import type { PlatformRole, PermissionTreeNode } from '@/types/api'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { formatDateTime } from '@/utils/business/format' import { fenToYuan, formatDateTime, yuanToFen } from '@/utils/business/format'
import { import {
CommonStatus, CommonStatus,
getStatusText, getStatusText,
@@ -343,9 +365,27 @@
role_name: '', role_name: '',
role_desc: '', role_desc: '',
role_type: 1, role_type: 1,
status: CommonStatus.ENABLED status: CommonStatus.ENABLED,
credit_enabled: false,
credit_limit_yuan: 0
}) })
const isCustomerRoleForm = computed(() => form.role_type === RoleType.CUSTOMER)
const handleDefaultCreditEnabledChange = (enabled: boolean | string | number) => {
if (!enabled) {
form.credit_limit_yuan = 0
}
}
const getDefaultCreditPayload = () => {
const creditEnabled = Boolean(form.credit_enabled)
return {
credit_enabled: creditEnabled,
credit_limit: creditEnabled ? yuanToFen(form.credit_limit_yuan) || 0 : 0
}
}
const roleList = ref<PlatformRole[]>([]) const roleList = ref<PlatformRole[]>([])
// 动态列配置 // 动态列配置
@@ -997,23 +1037,38 @@
const dialogType = ref('add') const dialogType = ref('add')
// 显示新增/编辑对话框 // 显示新增/编辑对话框
const showDialog = (type: string, row?: any) => { const showDialog = async (type: string, row?: any) => {
dialogVisible.value = true
dialogType.value = type dialogType.value = type
if (type === 'edit' && row) { if (type === 'edit' && row) {
form.id = row.ID let roleDetail = row
form.role_name = row.role_name try {
form.role_desc = row.role_desc const res = await RoleService.getRole(row.ID)
form.role_type = row.role_type if (res.code === 0 && res.data) {
form.status = row.status roleDetail = res.data
}
} catch (error) {
console.error('获取角色详情失败:', error)
}
form.id = roleDetail.ID
form.role_name = roleDetail.role_name
form.role_desc = roleDetail.role_desc
form.role_type = roleDetail.role_type
form.status = roleDetail.status
form.credit_enabled = Boolean(roleDetail.credit_enabled)
form.credit_limit_yuan = fenToYuan(roleDetail.credit_limit)
} else { } else {
form.id = 0 form.id = 0
form.role_name = '' form.role_name = ''
form.role_desc = '' form.role_desc = ''
form.role_type = 1 form.role_type = 1
form.status = CommonStatus.ENABLED form.status = CommonStatus.ENABLED
form.credit_enabled = false
form.credit_limit_yuan = 0
} }
dialogVisible.value = true
} }
// 删除角色 // 删除角色
@@ -1052,7 +1107,11 @@
role_type: form.role_type, role_type: form.role_type,
status: form.status status: form.status
} }
await RoleService.createRole(data) const res = await RoleService.createRole(data)
const createdRoleId = res.data?.ID ?? res.data?.id ?? res.data?.role_id
if (form.role_type === RoleType.CUSTOMER && createdRoleId) {
await RoleService.updateRoleDefaultCredit(createdRoleId, getDefaultCreditPayload())
}
ElMessage.success('新增成功') ElMessage.success('新增成功')
} else { } else {
// 更新角色时只发送允许的字段 // 更新角色时只发送允许的字段
@@ -1063,6 +1122,9 @@
status: form.status status: form.status
} }
await RoleService.updateRole(form.id, data) await RoleService.updateRole(form.id, data)
if (form.role_type === RoleType.CUSTOMER) {
await RoleService.updateRoleDefaultCredit(form.id, getDefaultCreditPayload())
}
ElMessage.success('修改成功') ElMessage.success('修改成功')
} }
@@ -1118,6 +1180,13 @@
} }
} }
.credit-form-tip {
margin-top: 6px;
font-size: 12px;
line-height: 1.4;
color: var(--el-text-color-secondary);
}
.permission-tree-transfer-container { .permission-tree-transfer-container {
display: flex; display: flex;
gap: 20px; gap: 20px;