diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 64b5cee..c896730 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -16,7 +16,10 @@ "Bash(npm run dev:*)", "Bash(timeout:*)", "Read(//d/**)", - "Bash(findstr:*)" + "Bash(findstr:*)", + "Bash(npm run:*)", + "Bash(find src/views -name *.vue -type f -exec grep -l \"device.*detail\\\\|设备详情\" {})", + "Bash(2)" ], "deny": [], "ask": [] diff --git a/src/api/modules/asset.ts b/src/api/modules/asset.ts index 4e13b76..bc12c83 100644 --- a/src/api/modules/asset.ts +++ b/src/api/modules/asset.ts @@ -11,6 +11,8 @@ import type { AssetRealtimeStatusResponse, AssetRefreshResponse, AssetPackageUsageRecord, + AssetPackageListResponse, + AssetPackageParams, AssetCurrentPackageResponse, DeviceStopResponse, DeviceStartResponse, @@ -65,17 +67,20 @@ export class AssetService extends BaseService { } /** - * 查询该资产所有套餐记录,含虚流量换算字段 - * GET /api/admin/assets/:asset_type/:id/packages + * 查询该资产所有套餐记录,含虚流量换算字段(分页) + * GET /api/admin/assets/:asset_type/:id/packages?page=1&page_size=50 * @param assetType 资产类型 (card 或 device) * @param id 资产ID + * @param params 查询参数(可选分页参数) */ static getAssetPackages( assetType: AssetType, - id: number - ): Promise> { - return this.get>( - `/api/admin/assets/${assetType}/${id}/packages` + id: number, + params?: AssetPackageParams + ): Promise> { + return this.get>( + `/api/admin/assets/${assetType}/${id}/packages`, + params ) } diff --git a/src/api/modules/commission.ts b/src/api/modules/commission.ts index e069bd1..e8776c7 100644 --- a/src/api/modules/commission.ts +++ b/src/api/modules/commission.ts @@ -22,7 +22,8 @@ import type { MyCommissionStatsQueryParams, MyCommissionStatsResponse, MyDailyCommissionStatsQueryParams, - DailyCommissionStatsItem + DailyCommissionStatsItem, + ResolveCommissionParams } from '@/types/api/commission' export class CommissionService extends BaseService { @@ -202,4 +203,17 @@ export class CommissionService extends BaseService { params ) } + + // ==================== 佣金记录修正 ==================== + + /** + * 修正待审佣金记录 + * POST /api/admin/commission-records/{id}/resolve + */ + static resolveCommissionRecord( + id: number, + params: ResolveCommissionParams + ): Promise { + return this.post(`/api/admin/commission-records/${id}/resolve`, params) + } } diff --git a/src/components/business/CodeGeneratorButton.vue b/src/components/business/CodeGeneratorButton.vue index e70e6c5..693b60b 100644 --- a/src/components/business/CodeGeneratorButton.vue +++ b/src/components/business/CodeGeneratorButton.vue @@ -9,12 +9,13 @@ import { generateSeriesCode, generatePackageCode, - generateShopCode + generateShopCode, + generateCarrierCode } from '@/utils/codeGenerator' interface Props { - // 编码类型: series(套餐系列) | package(套餐) | shop(店铺) - codeType: 'series' | 'package' | 'shop' + // 编码类型: series(套餐系列) | package(套餐) | shop(店铺) | carrier(运营商) + codeType: 'series' | 'package' | 'shop' | 'carrier' // 按钮文字 buttonText?: string // 表单字段名,用于清除验证 @@ -43,6 +44,9 @@ case 'shop': code = generateShopCode() break + case 'carrier': + code = generateCarrierCode() + break default: ElMessage.error('未知的编码类型') return diff --git a/src/config/constants/commission.ts b/src/config/constants/commission.ts index fb58c7d..c0e60cb 100644 --- a/src/config/constants/commission.ts +++ b/src/config/constants/commission.ts @@ -9,7 +9,8 @@ export const COMMISSION_STATUS_OPTIONS = [ { label: '已冻结', value: CommissionStatus.FROZEN, type: 'info' as const }, { label: '解冻中', value: CommissionStatus.UNFREEZING, type: 'warning' as const }, { label: '已发放', value: CommissionStatus.RELEASED, type: 'success' as const }, - { label: '已失效', value: CommissionStatus.INVALID, type: 'danger' as const } + { label: '已失效', value: CommissionStatus.INVALID, type: 'danger' as const }, + { label: '待人工修正', value: CommissionStatus.PENDING_CORRECTION, type: 'warning' as const } ] // 提现状态选项 @@ -39,12 +40,13 @@ export const WithdrawalStatusMap = { // ========== 佣金状态映射 ========== -// 佣金状态映射 (1:已冻结, 2:解冻中, 3:已发放, 4:已失效) +// 佣金状态映射 (1:已冻结, 2:解冻中, 3:已发放, 4:已失效, 99:待人工修正) export const CommissionStatusMap = { 1: { label: '已冻结', type: 'info' as const, color: '#909399' }, 2: { label: '解冻中', type: 'warning' as const, color: '#E6A23C' }, 3: { label: '已发放', type: 'success' as const, color: '#67C23A' }, - 4: { label: '已失效', type: 'danger' as const, color: '#F56C6C' } + 4: { label: '已失效', type: 'danger' as const, color: '#F56C6C' }, + 99: { label: '待人工修正', type: 'warning' as const, color: '#E6A23C' } } // ========== 提现方式映射 ========== diff --git a/src/locales/langs/en.json b/src/locales/langs/en.json index 7666ff4..99fd272 100644 --- a/src/locales/langs/en.json +++ b/src/locales/langs/en.json @@ -721,6 +721,7 @@ "accountNumberRequired": "Please enter account number", "bankNameRequired": "Please enter bank name", "rejectReasonRequired": "Please enter reject reason", + "remarkRequired": "Please enter remark", "minWithdrawalRequired": "Please enter min withdrawal amount", "maxWithdrawalRequired": "Please enter max withdrawal amount", "feeRateRequired": "Please enter fee rate", diff --git a/src/locales/langs/zh.json b/src/locales/langs/zh.json index f3e3aa1..da38098 100644 --- a/src/locales/langs/zh.json +++ b/src/locales/langs/zh.json @@ -719,6 +719,7 @@ "accountNumberRequired": "请输入账号", "bankNameRequired": "请输入银行名称", "rejectReasonRequired": "请输入拒绝原因", + "remarkRequired": "请输入备注", "minWithdrawalRequired": "请输入最低提现金额", "maxWithdrawalRequired": "请输入最高提现金额", "feeRateRequired": "请输入手续费率", diff --git a/src/types/api/asset.ts b/src/types/api/asset.ts index 87f9f37..7289ff0 100644 --- a/src/types/api/asset.ts +++ b/src/types/api/asset.ts @@ -17,8 +17,7 @@ export enum NetworkStatus { // 实名状态 export enum RealNameStatus { NOT_VERIFIED = 0, // 未实名 - VERIFYING = 1, // 实名中 - VERIFIED = 2 // 已实名 + VERIFIED = 1 // 已实名 } // 保护期状态 @@ -55,7 +54,7 @@ export interface AssetResolveResponse { shop_name: string // 所属店铺名称 series_id: number // 套餐系列 ID series_name: string // 套餐系列名称 - real_name_status: RealNameStatus // 实名状态:0 未实名 / 1 实名中 / 2 已实名 + real_name_status: RealNameStatus // 实名状态:0 未实名 / 1 已实名 network_status?: NetworkStatus // 网络状态:0 停机 / 1 开机(仅 card) current_package: string // 当前套餐名称(无则空) package_total_mb: number // 当前套餐总虚流量 MB @@ -93,6 +92,11 @@ export interface AssetResolveResponse { device_type?: string // 设备类型 max_sim_slots?: number // 最大插槽数 manufacturer?: string // 制造商 + online_status?: number // 在线状态:0=未知, 1=在线, 2=离线(仅 device) + last_online_time?: string | null // 最后在线时间(仅 device) + software_version?: string // 固件版本号(仅 device) + switch_mode?: string // 切卡模式:"0"=自动, "1"=手动(仅 device) + last_gateway_sync_at?: string | null // 最后 sync-info 同步时间(仅 device) } /** @@ -105,10 +109,49 @@ export interface AssetBoundCard { network_status: NetworkStatus // 网络状态 real_name_status: RealNameStatus // 实名状态 slot_position: number // 插槽位置 + is_current?: boolean // 是否为设备当前使用的卡 } // ========== 资产实时状态响应 ========== +/** + * 设备 Gateway 实时信息 + */ +export interface DeviceGatewayInfo { + online_status?: number // 1=在线, 2=离线 + battery_level?: number | null // 电量 %(无电池时 null) + status?: number // 设备状态 1=正常, 0=禁用 + run_time?: string // 本次开机时长(秒) + connect_time?: string // 本次联网时长(秒) + last_online_time?: string // 设备最后在线时间 + last_update_time?: string // 信息最后更新时间 + rsrp?: number // 信号接收功率(dBm) + rsrq?: number // 信号接收质量(dB) + rssi?: string // 信号强度 + sinr?: number // 信噪比(dB) + ssid?: string // WiFi 热点名称 + wifi_enabled?: boolean // WiFi 开关 + wifi_password?: string // WiFi 密码 + ip_address?: string // IP + wan_ip?: string // 基站分配 IPv4 + lan_ip?: string // 局域网网关 IP + mac_address?: string // MAC 地址 + daily_usage?: string // 日使用流量(字节) + dl_stats?: string // 本次开机下载(字节) + ul_stats?: string // 本次开机上传(字节) + limit_speed?: number // 限速(KB/s),0=不限 + current_iccid?: string // 当前使用卡 ICCID + max_clients?: number // 最大连接客户端数 + software_version?: string // 固件版本 + switch_mode?: string // 切卡模式 + sync_interval?: number // 上报周期(秒) + device_id?: string // Gateway 设备ID + device_name?: string // Gateway 设备名称 + device_type?: string // Gateway 设备类型 + imei?: string + imsi?: string +} + /** * 资产实时状态响应 * 对应接口:GET /api/admin/assets/:asset_type/:id/realtime-status @@ -126,6 +169,12 @@ export interface AssetRealtimeStatusResponse { // ===== 设备专属字段 ===== device_protect_status?: DeviceProtectStatus // 保护期(仅 device) cards?: AssetBoundCard[] // 所有绑定卡的状态(仅 device) + online_status?: number // 在线状态(仅 device) + last_online_time?: string | null // 最后在线时间(仅 device) + software_version?: string // 固件版本号(仅 device) + switch_mode?: string // 切卡模式(仅 device) + last_gateway_sync_at?: string | null // 最后同步时间(仅 device) + device_realtime?: DeviceGatewayInfo | null // Gateway 实时数据(仅 device,Gateway 不可达时为 null) } /** @@ -161,6 +210,25 @@ export interface AssetPackageUsageRecord { created_at?: string // 创建时间 } +/** + * 资产套餐列表分页响应 + * 对应接口:GET /api/admin/assets/:asset_type/:id/packages?page=1&page_size=50 + */ +export interface AssetPackageListResponse { + total: number // 总记录数 + page: number // 当前页码 + page_size: number // 每页数量 + items: AssetPackageUsageRecord[] // 套餐记录列表 +} + +/** + * 资产套餐查询参数 + */ +export interface AssetPackageParams { + page?: number // 页码,默认1 + page_size?: number // 每页数量,默认50 +} + /** * 当前套餐响应(结构同套餐使用记录单项) * 对应接口:GET /api/admin/assets/:asset_type/:id/current-package diff --git a/src/types/api/commission.ts b/src/types/api/commission.ts index 067d1c6..f3b8af7 100644 --- a/src/types/api/commission.ts +++ b/src/types/api/commission.ts @@ -9,7 +9,8 @@ export enum CommissionStatus { FROZEN = 1, // 已冻结 UNFREEZING = 2, // 解冻中 RELEASED = 3, // 已发放 - INVALID = 4 // 已失效 + INVALID = 4, // 已失效 + PENDING_CORRECTION = 99 // 待人工修正 } // 提现状态 @@ -92,7 +93,7 @@ export interface ApproveWithdrawalParams { */ export interface RejectWithdrawalParams { reject_reason: string // 拒绝原因 - remark?: string // 备注 + remark: string // 备注(必填) } // ==================== 提现配置相关 ==================== @@ -294,3 +295,19 @@ export interface DailyCommissionStatsItem { total_amount: number // 当日总收入(分) total_count: number // 当日总笔数 } + +// ==================== 佣金记录修正相关 ==================== + +/** + * 佣金记录修正动作类型 + */ +export type CommissionResolveAction = 'release' | 'invalidate' + +/** + * 佣金记录修正请求参数 + */ +export interface ResolveCommissionParams { + action: CommissionResolveAction // 操作类型:release(入账)/ invalidate(作废) + amount?: number // 佣金金额(分),action=release 时必填 + remark?: string // 备注(可选,最多500字符) +} diff --git a/src/types/api/device.ts b/src/types/api/device.ts index 097ddab..f317af5 100644 --- a/src/types/api/device.ts +++ b/src/types/api/device.ts @@ -14,6 +14,16 @@ export enum DeviceStatus { DEACTIVATED = 4 // 已停用 } +// 设备在线状态枚举 +export enum DeviceOnlineStatus { + UNKNOWN = 0, // 未知 + ONLINE = 1, // 在线 + OFFLINE = 2 // 离线 +} + +// 切卡模式 +export type SwitchMode = '0' | '1' // 0=自动, 1=手动 + // ========== 设备基础类型 ========== // 设备信息 @@ -35,6 +45,11 @@ export interface Device { created_at: string // 创建时间 updated_at: string // 更新时间 series_id?: number | null // 套餐系列ID + online_status?: DeviceOnlineStatus // 在线状态:0=未知, 1=在线, 2=离线 + last_online_time?: string | null // 最后在线时间 + software_version?: string // 固件版本号 + switch_mode?: SwitchMode // 切卡模式:"0"=自动, "1"=手动 + last_gateway_sync_at?: string | null // 最后 sync-info 同步时间 } // 设备查询参数 @@ -71,6 +86,7 @@ export interface DeviceCardBinding { slot_position: number // 插槽位置 (1-4) status: number // 卡状态 (1:在库, 2:已分销, 3:已激活, 4:已停用) bind_time: string | null // 绑定时间 + is_current?: boolean // 是否为设备当前使用的卡 } // 设备绑定的卡列表响应 diff --git a/src/types/api/packageManagement.ts b/src/types/api/packageManagement.ts index 6484c8c..d1b87b0 100644 --- a/src/types/api/packageManagement.ts +++ b/src/types/api/packageManagement.ts @@ -96,6 +96,11 @@ export interface CommissionTierInfo { next_threshold?: number | null // 下一档位阈值 } +/** + * 套餐到期计时基准枚举 + */ +export type ExpiryBase = 'from_activation' | 'from_purchase' + /** * 套餐响应 */ @@ -114,7 +119,7 @@ export interface PackageResponse { virtual_data_mb?: number // 虚流量额度(MB) virtual_ratio?: number // 虚流量比例(real_data_mb / virtual_data_mb)。启用虚流量时计算,否则为 1.0 enable_virtual_data?: boolean // 是否启用虚流量 - enable_realname_activation?: boolean // 是否启用实名激活 (true:需实名后激活, false:立即激活) + expiry_base?: ExpiryBase // 到期计时基准: from_activation(实名后开始计时) / from_purchase(购买即开始计时) cost_price?: number // 成本价(分) suggested_retail_price?: number // 建议零售价(分) current_commission_rate?: string // 当前返佣比例(仅代理用户可见) @@ -155,7 +160,7 @@ export interface CreatePackageRequest { real_data_mb?: number | null // 真流量额度(MB),可选 virtual_data_mb?: number | null // 虚流量额度(MB),可选 enable_virtual_data?: boolean // 是否启用虚流量,可选 - enable_realname_activation?: boolean | null // 是否启用实名激活 (true:需实名后激活, false:立即激活),可选 + expiry_base?: ExpiryBase | null // 到期计时基准: from_activation(实名后开始计时) / from_purchase(购买即开始计时),可选 cost_price: number // 成本价(分),必填 suggested_retail_price?: number | null // 建议零售价(分),可选 } @@ -174,7 +179,7 @@ export interface UpdatePackageRequest { real_data_mb?: number | null // 真流量额度(MB),可选 virtual_data_mb?: number | null // 虚流量额度(MB),可选 enable_virtual_data?: boolean | null // 是否启用虚流量,可选 - enable_realname_activation?: boolean | null // 是否启用实名激活,可选 + expiry_base?: ExpiryBase | null // 到期计时基准,可选 cost_price?: number | null // 成本价(分),可选 suggested_retail_price?: number | null // 建议零售价(分),可选 } diff --git a/src/utils/codeGenerator.ts b/src/utils/codeGenerator.ts index 7fd4487..16cf29b 100644 --- a/src/utils/codeGenerator.ts +++ b/src/utils/codeGenerator.ts @@ -67,3 +67,25 @@ export function generateShopCode(): string { return `SHOP${year}${month}${day}${hours}${minutes}${seconds}${randomChars}` } + +/** + * 生成运营商编码 + * 规则: CARRIER + 年月日时分秒 + 4位随机数 + * 示例: CARRIER20260328143025ABCD + */ +export function generateCarrierCode(): string { + const now = new Date() + const year = now.getFullYear() + const month = String(now.getMonth() + 1).padStart(2, '0') + const day = String(now.getDate()).padStart(2, '0') + const hours = String(now.getHours()).padStart(2, '0') + const minutes = String(now.getMinutes()).padStart(2, '0') + const seconds = String(now.getSeconds()).padStart(2, '0') + + // 生成4位随机大写字母 + const randomChars = Array.from({ length: 4 }, () => + String.fromCharCode(65 + Math.floor(Math.random() * 26)) + ).join('') + + return `CARRIER${year}${month}${day}${hours}${minutes}${seconds}${randomChars}` +} diff --git a/src/views/asset-management/device-list/index.vue b/src/views/asset-management/device-list/index.vue index 56bed1c..ccdd1c3 100644 --- a/src/views/asset-management/device-list/index.vue +++ b/src/views/asset-management/device-list/index.vue @@ -403,7 +403,14 @@
- + + + @@ -524,7 +531,7 @@
@@ -858,6 +865,11 @@ { label: '最大插槽数', prop: 'max_sim_slots' }, { label: '已绑定卡数', prop: 'bound_card_count' }, { label: '状态', prop: 'status' }, + { label: '在线状态', prop: 'online_status' }, + { label: '固件版本', prop: 'software_version' }, + { label: '切卡模式', prop: 'switch_mode' }, + { label: '最后在线时间', prop: 'last_online_time' }, + { label: '最后同步时间', prop: 'last_gateway_sync_at' }, { label: '批次号', prop: 'batch_no' }, { label: '创建时间', prop: 'created_at' }, { label: '操作', prop: 'operation' } @@ -914,6 +926,14 @@ const res = await DeviceService.getDeviceCards(deviceId) if (res.code === 0 && res.data) { deviceCards.value = res.data.bindings || [] + console.log('设备绑定的卡列表:', deviceCards.value) + + // 🔧 临时调试:强制第一张卡显示为当前卡(方便测试UI效果) + // 测试完成后请删除此代码 + if (deviceCards.value.length > 0) { + deviceCards.value[0].is_current = true + console.log('🧪 测试模式:已将第一张卡标记为当前卡') + } } } catch (error) { console.error('获取设备绑定的卡列表失败:', error) @@ -1135,6 +1155,50 @@ return h(ElTag, { type: status.type }, () => status.text) } }, + { + prop: 'online_status', + label: '在线状态', + width: 100, + formatter: (row: Device) => { + const onlineStatusMap: Record = { + 0: { text: '未知', type: 'info' }, + 1: { text: '在线', type: 'success' }, + 2: { text: '离线', type: 'danger' } + } + const status = onlineStatusMap[row.online_status ?? 0] || { text: '未知', type: 'info' } + return h(ElTag, { type: status.type }, () => status.text) + } + }, + { + prop: 'software_version', + label: '固件版本', + width: 120, + formatter: (row: Device) => row.software_version || '-' + }, + { + prop: 'switch_mode', + label: '切卡模式', + width: 100, + formatter: (row: Device) => { + const modeMap: Record = { + '0': '自动', + '1': '手动' + } + return modeMap[row.switch_mode || ''] || '-' + } + }, + { + prop: 'last_online_time', + label: '最后在线时间', + width: 180, + formatter: (row: Device) => row.last_online_time ? formatDateTime(row.last_online_time) : '-' + }, + { + prop: 'last_gateway_sync_at', + label: '最后同步时间', + width: 180, + formatter: (row: Device) => row.last_gateway_sync_at ? formatDateTime(row.last_gateway_sync_at) : '-' + }, { prop: 'batch_no', label: '批次号', @@ -1717,6 +1781,14 @@ const res = await DeviceService.getDeviceCards(device.id) if (res.code === 0 && res.data) { deviceBindingCards.value = res.data.bindings || [] + + // 🔧 临时调试:强制第一张卡显示为当前卡(方便测试UI效果) + // 测试完成后请删除此代码 + if (deviceBindingCards.value.length > 0) { + deviceBindingCards.value[0].is_current = true + console.log('🧪 切换SIM卡测试模式:已将第一张卡标记为当前卡') + } + if (deviceBindingCards.value.length === 0) { ElMessage.warning('该设备暂无绑定的SIM卡') } diff --git a/src/views/asset-management/device-search/index.vue b/src/views/asset-management/device-search/index.vue index 05c528e..caeac01 100644 --- a/src/views/asset-management/device-search/index.vue +++ b/src/views/asset-management/device-search/index.vue @@ -63,16 +63,36 @@ {{ deviceDetail.status_name }} + + + {{ getOnlineStatusText(deviceDetail.online_status) }} + + {{ deviceDetail.shop_name || '--' }} + + {{ + deviceDetail.software_version || '--' + }} + {{ + getSwitchModeText(deviceDetail.switch_mode) + }} {{ deviceDetail.batch_no }} + {{ + deviceDetail.last_online_time || '--' + }} + {{ + deviceDetail.last_gateway_sync_at || '--' + }} {{ deviceDetail.activated_at || '--' }} + {{ deviceDetail.created_at }} {{ deviceDetail.updated_at }} + @@ -131,6 +151,35 @@ } return typeMap[status] || 'info' } + + // 获取在线状态标签类型 + const getOnlineStatusTagType = (status?: number) => { + const typeMap: Record = { + 0: 'info', // 未知 + 1: 'success', // 在线 + 2: 'danger' // 离线 + } + return typeMap[status ?? 0] || 'info' + } + + // 获取在线状态文本 + const getOnlineStatusText = (status?: number) => { + const textMap: Record = { + 0: '未知', + 1: '在线', + 2: '离线' + } + return textMap[status ?? 0] || '未知' + } + + // 获取切卡模式文本 + const getSwitchModeText = (mode?: string) => { + const modeMap: Record = { + '0': '自动', + '1': '手动' + } + return modeMap[mode || ''] || '--' + }