feat(admin): 新增客服手动录单功能与仪表盘今日录单统计

- 做了什么:
  - 新增客服手动录单数据库迁移 000084 及对应模型,支持编号唯一校验、客服快照绑定与 CRUD 操作;
  - 后端提供 csmanualentry 业务模块及相关 API,并在仪表盘摘要中聚合今日各客服录单数量;
  - 前端增加客服手动录单视图页面、路由与侧边栏导航,支持录单二次确认与细粒度权限控制;
  - 管理后台仪表盘增加今日客服手动录单卡片展示。
- 为什么:
  - 满足客服在后台手动登记外部业务编号需求,并便于运营按客服统计今日录单业绩和追溯对账。
- 验证方式:
  - 执行后端全量测试 go test ./... 与静态检查 go vet ./... 全部通过;
  - 执行前端代码门禁 npm run check 与 vite build 全部通过;
  - 执行 goose down/up 迁移回滚与应用测试通过,角色权限分配正确;
  - 本地 dev 联调 POST /api/admin/manual-entries 录单成功返回 201。
This commit is contained in:
yml2213
2026-10-12 14:52:24 +08:00
parent 5840b7da40
commit cca40940d5
20 changed files with 1267 additions and 12 deletions
@@ -72,10 +72,17 @@ export interface DashboardRecentDispute {
created_at: string
}
export interface ManualEntryTodayStats {
admin_id: number
admin_name: string
count: number
}
export interface AdminDashboard {
metrics: DashboardMetrics
pending: DashboardPending
listing_daily: ListingDailyOverview
manual_entry_today: ManualEntryTodayStats[]
recent_orders: DashboardRecentOrder[]
recent_disputes: DashboardRecentDispute[]
generated_at: string
@@ -104,6 +111,7 @@ export async function fetchAdminDashboard() {
days: listingDaily?.days ?? 7,
timezone: listingDaily?.timezone ?? 'Asia/Shanghai',
},
manual_entry_today: payload.manual_entry_today ?? [],
recent_orders: payload.recent_orders ?? [],
recent_disputes: payload.recent_disputes ?? [],
}
@@ -0,0 +1,81 @@
import { apiClient } from '@/shared/api/client'
import type { ApiResponse, PaginatedResult } from '@/shared/types/types'
export interface ManualEntryItem {
id: number
entry_no: string
admin_id: number
admin_name: string
admin_username: string
remark: string
created_at: string
}
export interface ManualEntryAgentStats {
admin_id: number
admin_name: string
count: number
}
export interface CreateManualEntryRequest {
entry_no: string
remark?: string
}
export interface UpdateManualEntryRequest {
entry_no: string
remark?: string
}
export interface ManualEntryListQuery {
entry_no?: string
keyword?: string
admin_id?: number
start_date?: string
end_date?: string
}
export async function fetchManualEntries(
page = 1,
pageSize = 20,
query: ManualEntryListQuery = {}
) {
const { data } = await apiClient.get<ApiResponse<PaginatedResult<ManualEntryItem>>>(
'/admin/manual-entries',
{
params: { page, page_size: pageSize, ...query },
}
)
return data.data
}
export async function createManualEntry(payload: CreateManualEntryRequest) {
const { data } = await apiClient.post<ApiResponse<{ entry: ManualEntryItem }>>(
'/admin/manual-entries',
payload
)
return data.data
}
export async function updateManualEntry(id: number, payload: UpdateManualEntryRequest) {
const { data } = await apiClient.put<ApiResponse<ManualEntryItem>>(
`/admin/manual-entries/${id}`,
payload
)
return data.data
}
export async function deleteManualEntry(id: number) {
const { data } = await apiClient.delete<ApiResponse<{ deleted: boolean }>>(
`/admin/manual-entries/${id}`
)
return data.data
}
export async function fetchManualEntryTodayStats() {
const { data } = await apiClient.get<ApiResponse<ManualEntryAgentStats[]>>(
'/admin/manual-entries/today-stats'
)
return data.data
}