diff --git a/apps/backend/src/db/migrations/014_repair_worker_platform_rule_schema.sql b/apps/backend/src/db/migrations/014_repair_worker_platform_rule_schema.sql new file mode 100644 index 00000000..be48d86e --- /dev/null +++ b/apps/backend/src/db/migrations/014_repair_worker_platform_rule_schema.sql @@ -0,0 +1,19 @@ +-- 修复历史库中迁移记录已存在、但 008/009 的接单字段未实际落库的情况。 +-- 所有变更均幂等,正常完成过旧迁移的数据库不会受影响。 + +ALTER TABLE work_orders ADD COLUMN IF NOT EXISTS sharing_enabled BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE work_orders ADD COLUMN IF NOT EXISTS sharing_total_quantity INTEGER NOT NULL DEFAULT 1; +ALTER TABLE work_orders ADD COLUMN IF NOT EXISTS sharing_unit_reward INTEGER NOT NULL DEFAULT 0; +ALTER TABLE work_orders ADD COLUMN IF NOT EXISTS deadline_at TIMESTAMPTZ; +ALTER TABLE work_orders ADD COLUMN IF NOT EXISTS timeout_minutes INTEGER NOT NULL DEFAULT 0; +ALTER TABLE work_orders ADD COLUMN IF NOT EXISTS timeout_policy TEXT NOT NULL DEFAULT 'reopen'; + +ALTER TABLE work_product_rules ADD COLUMN IF NOT EXISTS sharing_enabled BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE work_product_rules ADD COLUMN IF NOT EXISTS sharing_total_quantity INTEGER NOT NULL DEFAULT 1; +ALTER TABLE work_product_rules ADD COLUMN IF NOT EXISTS sharing_unit_reward INTEGER NOT NULL DEFAULT 0; +ALTER TABLE work_product_rules ADD COLUMN IF NOT EXISTS timeout_minutes INTEGER NOT NULL DEFAULT 0; +ALTER TABLE work_product_rules ADD COLUMN IF NOT EXISTS timeout_policy TEXT NOT NULL DEFAULT 'reopen'; + +CREATE INDEX IF NOT EXISTS idx_work_orders_deadline + ON work_orders(status, deadline_at) + WHERE deadline_at IS NOT NULL; diff --git a/apps/backend/src/repositories/worker-platform/work-order-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-repo.ts index 9f9d14f1..2d562027 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-repo.ts @@ -391,6 +391,16 @@ export async function getWorkProductRuleByKey(ruleKey: string): Promise { + const result = await query<{ id: number }>( + 'DELETE FROM work_product_rules WHERE id = $1 RETURNING id', + [Number(ruleId)], + ) + return { deleted: Boolean(result.rows[0]) } +} + export async function upsertWorkProductRule(input: { ruleKey: string provider: string diff --git a/apps/backend/src/routes/admin/worker-platform.ts b/apps/backend/src/routes/admin/worker-platform.ts index bca155e9..daa5bf52 100644 --- a/apps/backend/src/routes/admin/worker-platform.ts +++ b/apps/backend/src/routes/admin/worker-platform.ts @@ -6,6 +6,7 @@ import { createAdminMockWorkOrder, deductAdminWorkOrderPendingDeposit, deleteAdminWorkCategory, + deleteAdminWorkProductRule, deleteAdminWorkOrder, deleteAdminWorkerLevel, getAdminWorkerFinanceConfig, @@ -157,6 +158,22 @@ router.post( }), ) +router.delete( + '/worker-platform/product-rules/:ruleId', + requireAdminRoles(['admin', 'operator']), + createJsonHandler((req) => deleteAdminWorkProductRule(String(req.params.ruleId || '')), { + successMessage: '接单物品规则已删除', + errorMessage: '删除接单物品规则失败', + scope: '[admin/worker-platform/product-rules/:ruleId]', + audit: (_req, data) => ({ + action: 'work_product_rule_deleted', + targetType: 'work_product_rule', + targetId: String(_req.params.ruleId || ''), + data: data && typeof data === 'object' ? (data as Record) : {}, + }), + }), +) + router.get( '/worker-platform/workers', requireAdminRoles(['admin', 'operator', 'support']), diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index e35fa22c..7c015b6d 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -21,6 +21,7 @@ import { createWorkerUser, deductPendingDepositUnfreeze, deleteWorkCategory, + deleteWorkProductRule, deleteWorkOrder, deleteWorkerLevel, getWorkerFinanceRequestById, @@ -293,6 +294,24 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) { return { rule: mapWorkProductRule(rule) } } +export async function deleteAdminWorkProductRule(ruleId: number | string) { + const normalizedRuleId = normalizeOptionalId(ruleId) + if (!normalizedRuleId) { + throw createHttpError('物品规则 ID 不正确', { + statusCode: 400, + errorCode: 'work_product_rule_id_invalid', + }) + } + const { deleted } = await deleteWorkProductRule(normalizedRuleId) + if (!deleted) { + throw createHttpError('物品规则不存在或已删除', { + statusCode: 404, + errorCode: 'work_product_rule_not_found', + }) + } + return { deleted } +} + export async function listAdminWorkerUsers(query: JsonObject = {}) { const page = normalizePage(query.page) const pageSize = normalizePageSize(query.pageSize) diff --git a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx index 78985c25..d16910ac 100644 --- a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx @@ -1,5 +1,7 @@ import { CheckOutlined, + DeleteOutlined, + EditOutlined, FormOutlined, PlusOutlined, ReloadOutlined, @@ -19,6 +21,7 @@ import { Input, InputNumber, Modal, + Popconfirm, Select, Space, Statistic, @@ -41,6 +44,7 @@ import { createAdminMockWorkOrder, creditAdminWorkerWallet, deleteAdminWorkCategory, + deleteAdminWorkProductRule, deleteAdminWorkerLevel, fetchAdminWorkerFinanceConfig, fetchAdminWorkerFinanceRequests, @@ -88,6 +92,7 @@ export default function ProductRulesPanel() { const { message } = App.useApp() const queryClient = useQueryClient() const [form] = Form.useForm() + const [editingRule, setEditingRule] = useState(null) const rulesQuery = useQuery({ queryKey: ['admin-worker-platform-product-rules'], queryFn: () => fetchAdminWorkProductRules(), @@ -121,8 +126,8 @@ export default function ProductRulesPanel() { }) { try { await saveAdminWorkProductRule(values) - message.success('物品规则已保存') - form.resetFields() + message.success(editingRule ? '物品规则已更新' : '物品规则已创建') + resetRuleForm() await Promise.all([ queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-product-rules'], @@ -136,6 +141,54 @@ export default function ProductRulesPanel() { } } + function resetRuleForm() { + setEditingRule(null) + form.resetFields() + } + + function editRule(rule: WorkProductRule) { + setEditingRule(rule) + form.setFieldsValue({ + ruleKey: rule.ruleKey, + provider: rule.provider, + platform: rule.platform, + shopId: rule.shopId, + skuCode: rule.skuCode, + productName: rule.productName, + matchType: rule.matchType, + categoryId: rule.categoryId || undefined, + rewardAmount: rule.rewardAmount / 100, + requiredDepositAmount: rule.requiredDepositAmount / 100, + depositThresholdAmount: rule.depositThresholdAmount / 100, + sharingEnabled: rule.sharing?.enabled || false, + sharingTotalQuantity: rule.sharing?.totalQuantity || 1, + sharingUnitReward: (rule.sharing?.unitReward || 0) / 100, + sharingTotalAmount: + ((rule.sharing?.totalQuantity || 1) * (rule.sharing?.unitReward || 0)) / 100, + timeoutMinutes: rule.timeoutMinutes || 0, + timeoutPolicy: rule.timeoutPolicy || 'reopen', + fieldsText: formatRuleFields(rule.requirement?.fields || []), + sortOrder: rule.sortOrder, + enabled: rule.enabled, + autoCreate: rule.autoCreate, + }) + } + + async function removeRule(rule: WorkProductRule) { + try { + await deleteAdminWorkProductRule(rule.ruleId) + if (editingRule?.ruleId === rule.ruleId) { + resetRuleForm() + } + message.success('物品规则已删除') + await queryClient.invalidateQueries({ + queryKey: ['admin-worker-platform-product-rules'], + }) + } catch (error) { + message.error(error instanceof Error ? error.message : '删除失败') + } + } + function syncSharingRuleForm(next: { sharingTotalQuantity?: number sharingUnitReward?: number @@ -224,11 +277,52 @@ export default function ProductRulesPanel() { ), }, + { + title: '操作', + key: 'actions', + width: 104, + fixed: 'right', + render: (_, row) => ( + + + ) : null + } + bordered={false} + >
- + @@ -424,9 +518,12 @@ export default function ProductRulesPanel() { ]} /> - + + + {!editingRule ? null : } +
{ + const options = Array.isArray(field.options) && field.options.length > 0 + ? `#${field.options.join(',')}` + : '' + return `${field.key}:${field.label}${options}` + }) + .join('\n') +} diff --git a/apps/frontend/src/services/admin/worker-platform.ts b/apps/frontend/src/services/admin/worker-platform.ts index 12478ad3..62abd605 100644 --- a/apps/frontend/src/services/admin/worker-platform.ts +++ b/apps/frontend/src/services/admin/worker-platform.ts @@ -115,6 +115,12 @@ export function saveAdminWorkProductRule(payload: { ) } +export function deleteAdminWorkProductRule(ruleId: number) { + return apiDelete<{ deleted: boolean }>( + `/api/v1/admin/worker-platform/product-rules/${ruleId}`, + ) +} + export function reviewAdminWorkerUser( workerId: number, payload: { status: string; reviewNote?: string; levelId?: number },