feat: 新增电工APP订单相关功能及数据库扩展
为四类订单表(报修/安装/维保/勘察)新增人工费、材料费、距离、预计时长字段,新增订单池、我的订单、订单详情、接单、完成订单、收益汇总接口,新增数据库迁移脚本和演示数据,支撑电工APP订单管理闭环
This commit is contained in:
1 parent
7b6fe46d10
commit
91aa3c06f0
8 files changed
+764
-3
No files matched your search
@@ -0,0 +1,69 @@
|
||||
-- 电工 APP(VoltLink)订单池 / 订单 / 收益功能扩展
|
||||
-- 背景:电工 APP 需要统一的「订单池 - 我的订单 - 收益」闭环。四类派单表
|
||||
-- (repair_request / install_order / maintenance_order / survey_order)此前缺少
|
||||
-- 报价(人工费/材料费)、距离、预计时长等展示字段,前端无法计算预估收益与列表展示。
|
||||
-- 本期为四张表补齐这些展示/计价字段(勘察复用已有 estimated_fee / actual_fee,不新增费用列),
|
||||
-- 并补充「按电工 + 状态」的联合查询索引,支撑订单池与收益统计。
|
||||
-- 幂等:新增列一律带 DEFAULT + IF NOT EXISTS;索引用 IF NOT EXISTS;重复执行安全。
|
||||
-- 注意:repair_request / install_order / maintenance_order 无 updated_at 列,收益时间以 created_at 兜底,
|
||||
-- 无需为此新增 updated_at(避免改动既有审计语义)。
|
||||
BEGIN;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 1) repair_request 表扩展:报价与展示字段
|
||||
-- ------------------------------------------------------------
|
||||
ALTER TABLE repair_request ADD COLUMN IF NOT EXISTS labor_fee NUMERIC(10,2) NOT NULL DEFAULT 0; -- 人工费(预估价,电工收益 = 人工费 + 材料费)
|
||||
ALTER TABLE repair_request ADD COLUMN IF NOT EXISTS material_fee NUMERIC(10,2) NOT NULL DEFAULT 0; -- 材料费(预估价)
|
||||
ALTER TABLE repair_request ADD COLUMN IF NOT EXISTS distance NUMERIC(6,2) NOT NULL DEFAULT 0; -- 距电工距离(km)
|
||||
ALTER TABLE repair_request ADD COLUMN IF NOT EXISTS estimated_duration VARCHAR(64); -- 预计时长文本,如 "1.5 - 2 小时"
|
||||
|
||||
COMMENT ON COLUMN repair_request.labor_fee IS '人工费(预估价,电工收益 = 人工费 + 材料费)';
|
||||
COMMENT ON COLUMN repair_request.material_fee IS '材料费(预估价)';
|
||||
COMMENT ON COLUMN repair_request.distance IS '距电工距离(km)';
|
||||
COMMENT ON COLUMN repair_request.estimated_duration IS '预计时长文本,如 "1.5 - 2 小时"';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 2) install_order 表扩展:报价与展示字段
|
||||
-- ------------------------------------------------------------
|
||||
ALTER TABLE install_order ADD COLUMN IF NOT EXISTS labor_fee NUMERIC(10,2) NOT NULL DEFAULT 0; -- 人工费(预估价)
|
||||
ALTER TABLE install_order ADD COLUMN IF NOT EXISTS material_fee NUMERIC(10,2) NOT NULL DEFAULT 0; -- 材料费(预估价)
|
||||
ALTER TABLE install_order ADD COLUMN IF NOT EXISTS distance NUMERIC(6,2) NOT NULL DEFAULT 0; -- 距电工距离(km)
|
||||
ALTER TABLE install_order ADD COLUMN IF NOT EXISTS estimated_duration VARCHAR(64); -- 预计时长文本
|
||||
|
||||
COMMENT ON COLUMN install_order.labor_fee IS '人工费(预估价,电工收益 = 人工费 + 材料费)';
|
||||
COMMENT ON COLUMN install_order.material_fee IS '材料费(预估价)';
|
||||
COMMENT ON COLUMN install_order.distance IS '距电工距离(km)';
|
||||
COMMENT ON COLUMN install_order.estimated_duration IS '预计时长文本,如 "1.5 - 2 小时"';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 3) maintenance_order 表扩展:报价与展示字段
|
||||
-- ------------------------------------------------------------
|
||||
ALTER TABLE maintenance_order ADD COLUMN IF NOT EXISTS labor_fee NUMERIC(10,2) NOT NULL DEFAULT 0; -- 人工费(预估价)
|
||||
ALTER TABLE maintenance_order ADD COLUMN IF NOT EXISTS material_fee NUMERIC(10,2) NOT NULL DEFAULT 0; -- 材料费(预估价)
|
||||
ALTER TABLE maintenance_order ADD COLUMN IF NOT EXISTS distance NUMERIC(6,2) NOT NULL DEFAULT 0; -- 距电工距离(km)
|
||||
ALTER TABLE maintenance_order ADD COLUMN IF NOT EXISTS estimated_duration VARCHAR(64); -- 预计时长文本
|
||||
|
||||
COMMENT ON COLUMN maintenance_order.labor_fee IS '人工费(预估价,电工收益 = 人工费 + 材料费)';
|
||||
COMMENT ON COLUMN maintenance_order.material_fee IS '材料费(预估价)';
|
||||
COMMENT ON COLUMN maintenance_order.distance IS '距电工距离(km)';
|
||||
COMMENT ON COLUMN maintenance_order.estimated_duration IS '预计时长文本,如 "1.5 - 2 小时"';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 4) survey_order 表扩展:距离与预计时长(金额复用 estimated_fee / actual_fee)
|
||||
-- ------------------------------------------------------------
|
||||
ALTER TABLE survey_order ADD COLUMN IF NOT EXISTS distance NUMERIC(6,2) NOT NULL DEFAULT 0; -- 距电工距离(km)
|
||||
ALTER TABLE survey_order ADD COLUMN IF NOT EXISTS estimated_duration VARCHAR(64); -- 预计时长文本
|
||||
|
||||
COMMENT ON COLUMN survey_order.distance IS '距电工距离(km)';
|
||||
COMMENT ON COLUMN survey_order.estimated_duration IS '预计时长文本,如 "1.5 - 2 小时"';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 5) 查询索引:订单池按「未接单 + 状态」过滤,我的订单/收益按「电工 + 状态」过滤
|
||||
-- 既有单列索引(worker_id / status)在多条件查询下选择性不足,补充联合索引。
|
||||
-- ------------------------------------------------------------
|
||||
CREATE INDEX IF NOT EXISTS idx_repair_disp_worker_status ON repair_dispatch (worker_id, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_install_worker_status ON install_order (worker_id, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_maint_worker_status ON maintenance_order (worker_id, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_survey_worker_status ON survey_order (worker_id, status);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,117 @@
|
||||
-- ============================================================
|
||||
-- 电工 APP(VoltLink)订单池演示数据
|
||||
--
|
||||
-- 依赖:
|
||||
-- 1) db_init.sql 基础表结构
|
||||
-- 2) 20261003_worker_app_orders.sql 报价/距离/时长等展示字段
|
||||
--
|
||||
-- 用法:
|
||||
-- psql -U postgres -d xintong -f database/seed_worker_app_orders.sql
|
||||
--
|
||||
-- 特性:
|
||||
-- - 独立事务(BEGIN/COMMIT)
|
||||
-- - 幂等可重复执行:全部使用「固定业务单号 + ON CONFLICT DO NOTHING」
|
||||
-- - 不写死主键,id 均由序列自增
|
||||
-- - 生成「未被任何电工接单」的订单池数据:报修 3 / 安装 2 / 维保 2 / 勘察 2
|
||||
-- ============================================================
|
||||
BEGIN;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 1. 演示客户(customer_no 唯一,作为全部演示订单的归属客户)
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO customer (customer_no, customer_name, province, city, district, address, contact_person, contact_phone, customer_level, status)
|
||||
VALUES ('KH-DEMO-0001', '示例智电科技园', '江苏省', '南京市', '江宁区', '江宁区智汇路 88 号', '张工', '13800000000', '企业', 3)
|
||||
ON CONFLICT (customer_no) DO NOTHING;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 2. 演示方案(install_order.plan_id 为必填外键,需保证存在可引用的 project_plan)
|
||||
-- 优先保证存在一条固定的演示方案;install_order 插入时再兜底取「任意已存在的方案」
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO project_plan (plan_no, customer_id, registrar, register_time, plan_desc, status)
|
||||
SELECT 'FA-DEMO-0001', c.id, '演示业务员', CURRENT_DATE, '电工 APP 订单池演示方案(自动生成)', 1
|
||||
FROM customer c
|
||||
WHERE c.customer_no = 'KH-DEMO-0001'
|
||||
ON CONFLICT (plan_no) DO NOTHING;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 3. 报修单(status=0 待处理,进入订单池)—— 3 条,金额各不相同
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO repair_request
|
||||
(repair_no, customer_id, requester, requester_phone, request_time, location, repair_content, photos, status, labor_fee, material_fee, distance, estimated_duration)
|
||||
SELECT 'BX-DEMO-0001', c.id, '张工', '13800000001', NOW() - INTERVAL '2 hour', 'A 栋 3 层配电间',
|
||||
'配电箱空开频繁跳闸,怀疑线路过载,请尽快上门排查处理', '[]'::jsonb, 0, 150.00, 80.00, 5.20, '1 - 1.5 小时'
|
||||
FROM customer c WHERE c.customer_no = 'KH-DEMO-0001'
|
||||
ON CONFLICT (repair_no) DO NOTHING;
|
||||
|
||||
INSERT INTO repair_request
|
||||
(repair_no, customer_id, requester, requester_phone, request_time, location, repair_content, photos, status, labor_fee, material_fee, distance, estimated_duration)
|
||||
SELECT 'BX-DEMO-0002', c.id, '李主管', '13800000002', NOW() - INTERVAL '5 hour', 'B 栋 1 层强电井',
|
||||
'楼层照明回路部分失电,需检测并更换损坏断路器', '[]'::jsonb, 0, 260.00, 120.00, 12.40, '2 - 3 小时'
|
||||
FROM customer c WHERE c.customer_no = 'KH-DEMO-0001'
|
||||
ON CONFLICT (repair_no) DO NOTHING;
|
||||
|
||||
INSERT INTO repair_request
|
||||
(repair_no, customer_id, requester, requester_phone, request_time, location, repair_content, photos, status, labor_fee, material_fee, distance, estimated_duration)
|
||||
SELECT 'BX-DEMO-0003', c.id, '王工', '13800000003', NOW() - INTERVAL '30 minute', 'C 栋 2 层办公室',
|
||||
'插座无电,需排查线路并更换面板', '[]'::jsonb, 0, 90.00, 35.00, 3.10, '1 小时以内'
|
||||
FROM customer c WHERE c.customer_no = 'KH-DEMO-0001'
|
||||
ON CONFLICT (repair_no) DO NOTHING;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 4. 安装派单(worker_id 为空,status=0 已派发,进入订单池)—— 2 条
|
||||
-- plan_id 优先取已存在的任意方案,其次回退到演示方案
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO install_order
|
||||
(install_no, customer_id, installer, worker_id, dispatch_date, start_date, end_date, plan_id, remark, status, labor_fee, material_fee, distance, estimated_duration)
|
||||
SELECT 'AZ-DEMO-0001', c.id, NULL, NULL, CURRENT_DATE, CURRENT_DATE, CURRENT_DATE + 3,
|
||||
COALESCE((SELECT id FROM project_plan ORDER BY id LIMIT 1), (SELECT id FROM project_plan WHERE plan_no = 'FA-DEMO-0001')),
|
||||
'智能电表安装 12 台,含桥架布线', 0, 480.00, 260.00, 12.50, '4 - 6 小时'
|
||||
FROM customer c WHERE c.customer_no = 'KH-DEMO-0001'
|
||||
ON CONFLICT (install_no) DO NOTHING;
|
||||
|
||||
INSERT INTO install_order
|
||||
(install_no, customer_id, installer, worker_id, dispatch_date, start_date, end_date, plan_id, remark, status, labor_fee, material_fee, distance, estimated_duration)
|
||||
SELECT 'AZ-DEMO-0002', c.id, NULL, NULL, CURRENT_DATE + 1, CURRENT_DATE + 1, CURRENT_DATE + 2,
|
||||
COALESCE((SELECT id FROM project_plan ORDER BY id LIMIT 1), (SELECT id FROM project_plan WHERE plan_no = 'FA-DEMO-0001')),
|
||||
'智能网关与采集器安装调试 3 套', 0, 320.00, 180.00, 6.80, '3 - 4 小时'
|
||||
FROM customer c WHERE c.customer_no = 'KH-DEMO-0001'
|
||||
ON CONFLICT (install_no) DO NOTHING;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 5. 维保派单(worker_id 为空,status=0 已派发,进入订单池)—— 2 条
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO maintenance_order
|
||||
(maintenance_no, customer_id, maintainer, worker_id, maintenance_date, remark, status, labor_fee, material_fee, distance, estimated_duration)
|
||||
SELECT 'WB-DEMO-0001', c.id, NULL, NULL, CURRENT_DATE + 2,
|
||||
'季度维保:配电柜除尘、紧固、绝缘检测', 0, 200.00, 60.00, 8.30, '2 - 3 小时'
|
||||
FROM customer c WHERE c.customer_no = 'KH-DEMO-0001'
|
||||
ON CONFLICT (maintenance_no) DO NOTHING;
|
||||
|
||||
INSERT INTO maintenance_order
|
||||
(maintenance_no, customer_id, maintainer, worker_id, maintenance_date, remark, status, labor_fee, material_fee, distance, estimated_duration)
|
||||
SELECT 'WB-DEMO-0002', c.id, NULL, NULL, CURRENT_DATE + 5,
|
||||
'年度维保:变压器巡检与接地电阻测试', 0, 180.00, 40.00, 15.60, '2 小时'
|
||||
FROM customer c WHERE c.customer_no = 'KH-DEMO-0001'
|
||||
ON CONFLICT (maintenance_no) DO NOTHING;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 6. 勘察派单(worker_id 为空,status=0 待指派,is_draft=0,进入订单池)—— 2 条
|
||||
-- 金额复用 estimated_fee / actual_fee,不新增费用列
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO survey_order
|
||||
(survey_no, customer_id, surveyor, worker_id, survey_time, priority, task_title, site_location, source, tech_contact,
|
||||
plan_start, plan_end, is_draft, estimated_fee, actual_fee, photos, remark, status, distance, estimated_duration)
|
||||
SELECT 'KC-DEMO-0001', c.id, NULL, NULL, NOW(), 3, '新增大功率充电桩勘察', 'C 栋地下车库', 'ledger', '13800000002',
|
||||
NOW() + INTERVAL '1 day', NOW() + INTERVAL '1 day 2 hour', 0, 260.00, NULL, '[]', '需评估配电容量与布线路径', 0, 8.30, '2 - 3 小时'
|
||||
FROM customer c WHERE c.customer_no = 'KH-DEMO-0001'
|
||||
ON CONFLICT (survey_no) DO NOTHING;
|
||||
|
||||
INSERT INTO survey_order
|
||||
(survey_no, customer_id, surveyor, worker_id, survey_time, priority, task_title, site_location, source, tech_contact,
|
||||
plan_start, plan_end, is_draft, estimated_fee, actual_fee, photos, remark, status, distance, estimated_duration)
|
||||
SELECT 'KC-DEMO-0002', c.id, NULL, NULL, NOW(), 3, '配电房扩容勘察', 'A 栋 1 层配电房', 'ledger', '13800000003',
|
||||
NOW() + INTERVAL '2 day', NOW() + INTERVAL '2 day 2 hour', 0, 180.00, NULL, '[]', '需确认扩容后回路数与母线规格', 0, 4.60, '1.5 - 2 小时'
|
||||
FROM customer c WHERE c.customer_no = 'KH-DEMO-0001'
|
||||
ON CONFLICT (survey_no) DO NOTHING;
|
||||
|
||||
COMMIT;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Controller, Get, Post, Put, Del, Inject, Body, Param } from "@midwayjs/core";
|
||||
import { Controller, Get, Post, Put, Del, Inject, Body, Param, Query } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { WorkerAppService } from "../service/worker_app.service";
|
||||
import {
|
||||
@@ -72,6 +72,51 @@ export class WorkerAppController {
|
||||
return this.workerAppService.listNotices(this.currentWorkerId());
|
||||
}
|
||||
|
||||
// 订单池:静态路径必须声明在动态路径 /orders/:source/:id 之前
|
||||
@Get("/orders/pool")
|
||||
async orderPool() {
|
||||
return this.workerAppService.orderPool();
|
||||
}
|
||||
|
||||
// 我的订单(可按 status=pending|completed 过滤)
|
||||
@Get("/orders")
|
||||
async myOrders(@Query("status") status?: string) {
|
||||
return this.workerAppService.myOrders(this.currentWorkerId(), status);
|
||||
}
|
||||
|
||||
// 订单详情
|
||||
@Get("/orders/:source/:id")
|
||||
async orderDetail(@Param("source") source: string, @Param("id") id: string) {
|
||||
if (!this.workerAppService.isValidSource(source)) {
|
||||
return { code: 400, message: "不支持的订单来源" };
|
||||
}
|
||||
return this.workerAppService.orderDetail(this.currentWorkerId(), source, Number(id));
|
||||
}
|
||||
|
||||
// 接单
|
||||
@Post("/orders/:source/:id/grab")
|
||||
async grabOrder(@Param("source") source: string, @Param("id") id: string) {
|
||||
if (!this.workerAppService.isValidSource(source)) {
|
||||
return { code: 400, message: "不支持的订单来源" };
|
||||
}
|
||||
return this.workerAppService.grabOrder(this.currentWorkerId(), source, Number(id));
|
||||
}
|
||||
|
||||
// 完成订单
|
||||
@Put("/orders/:source/:id/complete")
|
||||
async completeOrder(@Param("source") source: string, @Param("id") id: string) {
|
||||
if (!this.workerAppService.isValidSource(source)) {
|
||||
return { code: 400, message: "不支持的订单来源" };
|
||||
}
|
||||
return this.workerAppService.completeOrder(this.currentWorkerId(), source, Number(id));
|
||||
}
|
||||
|
||||
// 收益汇总
|
||||
@Get("/earnings/summary")
|
||||
async earningsSummary() {
|
||||
return this.workerAppService.earningsSummary(this.currentWorkerId());
|
||||
}
|
||||
|
||||
// 当前登录电工 ID(由 WorkerAuthMiddleware 写入)
|
||||
private currentWorkerId(): number {
|
||||
return this.ctx.state.worker?.workerId;
|
||||
|
||||
@@ -52,6 +52,18 @@ export default class InstallOrder extends Model {
|
||||
@Column({ type: DataType.SMALLINT, allowNull: false, defaultValue: 0, comment: "状态:0已派发 1已完成" })
|
||||
status!: number;
|
||||
|
||||
@Column({ type: DataType.DECIMAL(10, 2), allowNull: false, defaultValue: 0, field: "labor_fee", comment: "人工费(电工收益 = 人工费 + 材料费)" })
|
||||
laborFee!: number;
|
||||
|
||||
@Column({ type: DataType.DECIMAL(10, 2), allowNull: false, defaultValue: 0, field: "material_fee", comment: "材料费" })
|
||||
materialFee!: number;
|
||||
|
||||
@Column({ type: DataType.DECIMAL(6, 2), allowNull: false, defaultValue: 0, field: "distance", comment: "距电工距离(km)" })
|
||||
distance!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), field: "estimated_duration", comment: "预计时长文本" })
|
||||
estimatedDuration!: string;
|
||||
|
||||
@BelongsTo(() => Customer, { foreignKey: "customerId", as: "customer" })
|
||||
customer!: Customer;
|
||||
|
||||
|
||||
@@ -38,6 +38,18 @@ export default class MaintenanceOrder extends Model {
|
||||
@Column({ type: DataType.SMALLINT, allowNull: false, defaultValue: 0, comment: "状态:0已派发 1已完成" })
|
||||
status!: number;
|
||||
|
||||
@Column({ type: DataType.DECIMAL(10, 2), allowNull: false, defaultValue: 0, field: "labor_fee", comment: "人工费(电工收益 = 人工费 + 材料费)" })
|
||||
laborFee!: number;
|
||||
|
||||
@Column({ type: DataType.DECIMAL(10, 2), allowNull: false, defaultValue: 0, field: "material_fee", comment: "材料费" })
|
||||
materialFee!: number;
|
||||
|
||||
@Column({ type: DataType.DECIMAL(6, 2), allowNull: false, defaultValue: 0, field: "distance", comment: "距电工距离(km)" })
|
||||
distance!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), field: "estimated_duration", comment: "预计时长文本" })
|
||||
estimatedDuration!: string;
|
||||
|
||||
@BelongsTo(() => Customer, { foreignKey: "customerId", as: "customer" })
|
||||
customer!: Customer;
|
||||
|
||||
|
||||
@@ -48,6 +48,18 @@ export default class RepairRequest extends Model {
|
||||
@Column({ type: DataType.SMALLINT, allowNull: false, defaultValue: 0, comment: "状态:0待处理 1已派单 2已完成" })
|
||||
status!: number;
|
||||
|
||||
@Column({ type: DataType.DECIMAL(10, 2), allowNull: false, defaultValue: 0, field: "labor_fee", comment: "人工费(电工收益 = 人工费 + 材料费)" })
|
||||
laborFee!: number;
|
||||
|
||||
@Column({ type: DataType.DECIMAL(10, 2), allowNull: false, defaultValue: 0, field: "material_fee", comment: "材料费" })
|
||||
materialFee!: number;
|
||||
|
||||
@Column({ type: DataType.DECIMAL(6, 2), allowNull: false, defaultValue: 0, field: "distance", comment: "距电工距离(km)" })
|
||||
distance!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), field: "estimated_duration", comment: "预计时长文本" })
|
||||
estimatedDuration!: string;
|
||||
|
||||
@BelongsTo(() => Customer, { foreignKey: "customerId", as: "customer" })
|
||||
customer!: Customer;
|
||||
|
||||
|
||||
@@ -119,6 +119,12 @@ export default class SurveyOrder extends Model {
|
||||
@Column({ type: DataType.SMALLINT, allowNull: false, defaultValue: 0, comment: "状态:0待指派 1现场勘察中 2待技术复核 3已归档 4预留 5已取消" })
|
||||
status!: number;
|
||||
|
||||
@Column({ type: DataType.DECIMAL(6, 2), allowNull: false, defaultValue: 0, field: "distance", comment: "距电工距离(km)" })
|
||||
distance!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), field: "estimated_duration", comment: "预计时长文本" })
|
||||
estimatedDuration!: string;
|
||||
|
||||
@BelongsTo(() => Customer, { foreignKey: "customerId", as: "customer" })
|
||||
customer!: Customer;
|
||||
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { InjectRepository, InjectDataSource } from "@midwayjs/sequelize";
|
||||
import { Repository } from "sequelize-typescript";
|
||||
import { Op, Sequelize } from "sequelize";
|
||||
import dayjs from "dayjs";
|
||||
import Worker from "../entity/worker.entity";
|
||||
import WorkerCert from "../entity/worker_cert.entity";
|
||||
import SysNotice from "../entity/sys_notice.entity";
|
||||
import RepairRequest from "../entity/repair_request.entity";
|
||||
import RepairDispatch from "../entity/repair_dispatch.entity";
|
||||
import InstallOrder from "../entity/install_order.entity";
|
||||
import MaintenanceOrder from "../entity/maintenance_order.entity";
|
||||
import SurveyOrder from "../entity/survey_order.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import { TokenUtil } from "../util/token";
|
||||
import { hashPassword, comparePassword } from "../util/crypto";
|
||||
import {
|
||||
@@ -20,6 +28,18 @@ const NOTICE_TYPE_CERT_EXPIRE = "cert_expire";
|
||||
// 证照业务类型
|
||||
const BIZ_TYPE_WORKER_CERT = "worker_cert";
|
||||
|
||||
// 订单来源元数据:业务类型(前端标签)+ 业务名(收益流水)+ 图标(前端已按此对接)
|
||||
const ORDER_SOURCE_META: any = {
|
||||
repair: { type: "urgent", name: "报修维修", icon: "bolt" },
|
||||
install: { type: "standard", name: "安装", icon: "construction" },
|
||||
maintenance: { type: "regular", name: "维保", icon: "schedule" },
|
||||
survey: { type: "regular", name: "勘察", icon: "search" }
|
||||
};
|
||||
// 合法的订单来源
|
||||
const ORDER_SOURCES = ["repair", "install", "maintenance", "survey"];
|
||||
// 订单池/我的订单标题最大展示长度(超出截断,避免前端换行错乱)
|
||||
const TITLE_MAX = 24;
|
||||
|
||||
@Provide()
|
||||
export class WorkerAppService {
|
||||
@Inject()
|
||||
@@ -34,6 +54,25 @@ export class WorkerAppService {
|
||||
@InjectRepository(SysNotice)
|
||||
noticeModel!: Repository<SysNotice>;
|
||||
|
||||
@InjectRepository(RepairRequest)
|
||||
requestModel!: Repository<RepairRequest>;
|
||||
|
||||
@InjectRepository(RepairDispatch)
|
||||
dispatchModel!: Repository<RepairDispatch>;
|
||||
|
||||
@InjectRepository(InstallOrder)
|
||||
installModel!: Repository<InstallOrder>;
|
||||
|
||||
@InjectRepository(MaintenanceOrder)
|
||||
maintenanceModel!: Repository<MaintenanceOrder>;
|
||||
|
||||
@InjectRepository(SurveyOrder)
|
||||
surveyModel!: Repository<SurveyOrder>;
|
||||
|
||||
// 接单/完成需要多表原子更新,使用事务保证并发下不重复接单、不重复计数
|
||||
@InjectDataSource()
|
||||
sequelize!: Sequelize;
|
||||
|
||||
// 电工注册:手机号唯一校验,初始为待审核(0)且离职(0)
|
||||
async register(dto: WorkerRegisterDTO): Promise<any> {
|
||||
const exist = await this.workerModel.findOne({ where: { phone: dto.phone } });
|
||||
@@ -250,6 +289,452 @@ export class WorkerAppService {
|
||||
return { code: 200, message: "获取成功", data: list };
|
||||
}
|
||||
|
||||
// ================= 订单池 / 我的订单 / 订单详情 =================
|
||||
|
||||
// 订单来源是否合法(控制器据此返回 400)
|
||||
isValidSource(source: string): boolean {
|
||||
return ORDER_SOURCES.includes(source);
|
||||
}
|
||||
|
||||
// 订单池:四类「未被任何电工接单」的订单合并,按发布时间倒序
|
||||
async orderPool(): Promise<any> {
|
||||
const [repairs, installs, maintenances, surveys] = await Promise.all([
|
||||
this.requestModel.findAll({ where: { status: 0 }, include: [this.customerInclude()] }),
|
||||
this.installModel.findAll({ where: { workerId: null, status: 0 }, include: [this.customerInclude()] }),
|
||||
this.maintenanceModel.findAll({ where: { workerId: null, status: 0 }, include: [this.customerInclude()] }),
|
||||
// 勘察草稿不进入订单池
|
||||
this.surveyModel.findAll({ where: { workerId: null, status: 0, isDraft: 0 }, include: [this.customerInclude()] })
|
||||
]);
|
||||
|
||||
const merged = [
|
||||
...repairs.map(row => ({ row, source: "repair" })),
|
||||
...installs.map(row => ({ row, source: "install" })),
|
||||
...maintenances.map(row => ({ row, source: "maintenance" })),
|
||||
...surveys.map(row => ({ row, source: "survey" }))
|
||||
];
|
||||
// 四类订单合并后统一按创建时间倒序,最新发布的订单优先展示
|
||||
merged.sort((a, b) => this.createdTime(b.row) - this.createdTime(a.row));
|
||||
|
||||
const list = merged.map(item => this.toOrderView(item.row.toJSON(), item.source));
|
||||
return { code: 200, message: "获取成功", data: { total: list.length, list } };
|
||||
}
|
||||
|
||||
// 我的订单:当前电工已接单,可按 pending/completed 过滤
|
||||
async myOrders(workerId: number, status?: string): Promise<any> {
|
||||
const collected: any[] = [];
|
||||
|
||||
// 报修:经 repair_dispatch 反查当前电工已接的报修单
|
||||
const dispatches = await this.dispatchModel.findAll({
|
||||
where: { workerId },
|
||||
include: [
|
||||
{
|
||||
model: RepairRequest,
|
||||
as: "repair",
|
||||
required: true,
|
||||
include: [this.customerInclude()]
|
||||
}
|
||||
]
|
||||
});
|
||||
for (const dispatch of dispatches as any[]) {
|
||||
const repair = dispatch.repair;
|
||||
if (!repair) continue;
|
||||
const orderStatus = Number(repair.status);
|
||||
if (status === "pending" && orderStatus >= 2) continue;
|
||||
if (status === "completed" && orderStatus !== 2) continue;
|
||||
collected.push({ row: repair, source: "repair" });
|
||||
}
|
||||
|
||||
// 安装/维保:worker_id 直接归属当前电工;pending=已派发(0),completed=已完成(1)
|
||||
const workWhere: any = { workerId };
|
||||
if (status === "pending") workWhere.status = 0;
|
||||
if (status === "completed") workWhere.status = 1;
|
||||
const [installs, maintenances] = await Promise.all([
|
||||
this.installModel.findAll({ where: { ...workWhere }, include: [this.customerInclude()] }),
|
||||
this.maintenanceModel.findAll({ where: { ...workWhere }, include: [this.customerInclude()] })
|
||||
]);
|
||||
installs.forEach(row => collected.push({ row, source: "install" }));
|
||||
maintenances.forEach(row => collected.push({ row, source: "maintenance" }));
|
||||
|
||||
// 勘察:pending=现场勘察中(1)/待技术复核(2);completed=已归档(3)
|
||||
const surveyWhere: any = { workerId };
|
||||
if (status === "pending") surveyWhere.status = { [Op.in]: [1, 2] };
|
||||
if (status === "completed") surveyWhere.status = 3;
|
||||
const surveys = await this.surveyModel.findAll({ where: surveyWhere, include: [this.customerInclude()] });
|
||||
surveys.forEach(row => collected.push({ row, source: "survey" }));
|
||||
|
||||
collected.sort((a, b) => this.createdTime(b.row) - this.createdTime(a.row));
|
||||
const list = collected.map(item => this.toOrderView(item.row.toJSON(), item.source));
|
||||
return { code: 200, message: "获取成功", data: { total: list.length, list } };
|
||||
}
|
||||
|
||||
// 订单详情:单条完整视图(description 不截断)
|
||||
async orderDetail(workerId: number, source: string, id: number): Promise<any> {
|
||||
const row: any = await this.sourceModel(source).findByPk(id, { include: [this.customerInclude()] });
|
||||
if (!row) return { code: 404, message: "订单不存在" };
|
||||
return { code: 200, message: "获取成功", data: this.toOrderView(row.toJSON(), source) };
|
||||
}
|
||||
|
||||
// ================= 接单 / 完成 =================
|
||||
|
||||
// 接单:事务 + 条件更新做并发防护,affected 为 0 即已被他人抢走
|
||||
async grabOrder(workerId: number, source: string, id: number): Promise<any> {
|
||||
if (source === "repair") return this.grabRepair(workerId, id);
|
||||
|
||||
const affected = await this.sequelize.transaction(async t => {
|
||||
// 勘察接单需同时进入「现场勘察中」
|
||||
const payload: any = source === "survey" ? { workerId, status: 1 } : { workerId };
|
||||
const [count] = await this.sourceModel(source).update(payload, {
|
||||
where: { id, workerId: null },
|
||||
transaction: t
|
||||
});
|
||||
return count;
|
||||
});
|
||||
|
||||
if (!affected) return { code: 409, message: "订单已被他人接单" };
|
||||
return { code: 200, message: "接单成功", data: { id: `${source}-${id}`, source } };
|
||||
}
|
||||
|
||||
// 报修接单:需额外创建 repair_dispatch 并回写报修单状态
|
||||
private async grabRepair(workerId: number, id: number): Promise<any> {
|
||||
const result = await this.sequelize.transaction(async t => {
|
||||
// 已有派单说明该报修已被接单,直接判定冲突
|
||||
const existed = await this.dispatchModel.findOne({ where: { repairId: id }, transaction: t });
|
||||
if (existed) return { code: 409, message: "订单已被他人接单" };
|
||||
|
||||
// 原子抢占:仅当报修单仍为待处理(0)时才更新成功,避免并发下重复接单
|
||||
const [affected] = await this.requestModel.update({ status: 1 }, { where: { id, status: 0 }, transaction: t });
|
||||
if (!affected) return { code: 409, message: "订单已被他人接单" };
|
||||
|
||||
const repair = await this.requestModel.findByPk(id, { transaction: t });
|
||||
// 理论上前置更新成功即存在,此处兜底避免脏数据导致空指针并回滚
|
||||
if (!repair) return { code: 409, message: "订单已被他人接单" };
|
||||
const dispatchNo = await this.generateDispatchNo(t);
|
||||
// 派单表冗余报修单关键字段,便于管理端派单列表直接读取
|
||||
await this.dispatchModel.create(
|
||||
{
|
||||
dispatchNo,
|
||||
repairId: repair.id,
|
||||
customerId: repair.customerId,
|
||||
requester: repair.requester,
|
||||
requesterPhone: repair.requesterPhone,
|
||||
requestTime: repair.requestTime,
|
||||
buildingId: repair.buildingId,
|
||||
floorNo: repair.floorNo,
|
||||
location: repair.location,
|
||||
repairContent: repair.repairContent,
|
||||
workerId,
|
||||
status: 0
|
||||
} as any,
|
||||
{ transaction: t }
|
||||
);
|
||||
|
||||
return { code: 200 };
|
||||
});
|
||||
|
||||
if (result.code !== 200) return result;
|
||||
return { code: 200, message: "接单成功", data: { id: `repair-${id}`, source: "repair" } };
|
||||
}
|
||||
|
||||
// 完成订单:校验归属(不属于当前电工返回 403)
|
||||
async completeOrder(workerId: number, source: string, id: number): Promise<any> {
|
||||
if (source === "repair") return this.completeRepair(workerId, id);
|
||||
|
||||
const row: any = await this.sourceModel(source).findByPk(id);
|
||||
if (!row) return { code: 404, message: "订单不存在" };
|
||||
if (Number(row.workerId) !== Number(workerId)) return { code: 403, message: "无权操作该订单" };
|
||||
|
||||
// 勘察完成进入「待技术复核(2)」,其余进入「已完成(1)」
|
||||
const targetStatus = source === "survey" ? 2 : 1;
|
||||
const isTransition = Number(row.status) !== targetStatus;
|
||||
await this.sequelize.transaction(async t => {
|
||||
await row.update({ status: targetStatus }, { transaction: t });
|
||||
if (isTransition) await this.bumpCompletedCount(workerId, t);
|
||||
});
|
||||
return { code: 200, message: "已完成" };
|
||||
}
|
||||
|
||||
// 报修完成:派单置已处理 + 报修单置已完成
|
||||
private async completeRepair(workerId: number, id: number): Promise<any> {
|
||||
const dispatch = await this.dispatchModel.findOne({ where: { repairId: id } });
|
||||
if (!dispatch) return { code: 404, message: "订单不存在" };
|
||||
if (Number(dispatch.workerId) !== Number(workerId)) return { code: 403, message: "无权操作该订单" };
|
||||
|
||||
const repair = await this.requestModel.findByPk(id);
|
||||
if (!repair) return { code: 404, message: "订单不存在" };
|
||||
|
||||
// 仅「未完成 → 完成」的首次完成才累计完成数,重复调用不重复计数
|
||||
const isTransition = Number(dispatch.status) !== 1 || Number(repair.status) !== 2;
|
||||
await this.sequelize.transaction(async t => {
|
||||
await dispatch.update({ status: 1, completedAt: new Date() }, { transaction: t });
|
||||
await repair.update({ status: 2 }, { transaction: t });
|
||||
if (isTransition) await this.bumpCompletedCount(workerId, t);
|
||||
});
|
||||
return { code: 200, message: "已完成" };
|
||||
}
|
||||
|
||||
// ================= 收益汇总 =================
|
||||
|
||||
// 收益汇总:累计/可提现/本月/近 7 日趋势/最近 20 条流水
|
||||
async earningsSummary(workerId: number): Promise<any> {
|
||||
const completed: any[] = [];
|
||||
|
||||
// 报修:以派单已处理(1)判定完成,完成时间取 dispatch.completed_at,缺失回退报修单创建时间
|
||||
const dispatches = await this.dispatchModel.findAll({
|
||||
where: { workerId, status: 1 },
|
||||
include: [{ model: RepairRequest, as: "repair" }]
|
||||
});
|
||||
for (const dispatch of dispatches as any[]) {
|
||||
const repair = dispatch.repair;
|
||||
if (!repair) continue;
|
||||
completed.push({
|
||||
source: "repair",
|
||||
rowId: repair.id,
|
||||
jobId: repair.repairNo,
|
||||
amount: this.toMoney(this.toMoney(repair.laborFee) + this.toMoney(repair.materialFee)),
|
||||
time: dispatch.completedAt || repair.created_at
|
||||
});
|
||||
}
|
||||
|
||||
const [installs, maintenances, surveys] = await Promise.all([
|
||||
this.installModel.findAll({ where: { workerId, status: 1 } }),
|
||||
this.maintenanceModel.findAll({ where: { workerId, status: 1 } }),
|
||||
// 勘察:电工完成(2 待技术复核)或已归档(3) 均视为已完成收益
|
||||
this.surveyModel.findAll({ where: { workerId, status: { [Op.in]: [2, 3] } } })
|
||||
]);
|
||||
for (const row of installs as any[]) {
|
||||
completed.push({
|
||||
source: "install",
|
||||
rowId: row.id,
|
||||
jobId: row.installNo,
|
||||
amount: this.toMoney(this.toMoney(row.laborFee) + this.toMoney(row.materialFee)),
|
||||
time: row.updated_at || row.created_at
|
||||
});
|
||||
}
|
||||
for (const row of maintenances as any[]) {
|
||||
completed.push({
|
||||
source: "maintenance",
|
||||
rowId: row.id,
|
||||
jobId: row.maintenanceNo,
|
||||
amount: this.toMoney(this.toMoney(row.laborFee) + this.toMoney(row.materialFee)),
|
||||
time: row.updated_at || row.created_at
|
||||
});
|
||||
}
|
||||
for (const row of surveys as any[]) {
|
||||
completed.push({
|
||||
source: "survey",
|
||||
rowId: row.id,
|
||||
jobId: row.surveyNo,
|
||||
amount: this.toMoney(row.actualFee ?? row.estimatedFee ?? 0),
|
||||
time: row.updated_at || row.created_at
|
||||
});
|
||||
}
|
||||
|
||||
const totalBalance = this.toMoney(completed.reduce((sum, item) => sum + item.amount, 0));
|
||||
const now = dayjs();
|
||||
const monthlyEarnings = this.toMoney(
|
||||
completed
|
||||
.filter(item => item.time && dayjs(item.time).isSame(now, "month"))
|
||||
.reduce((sum, item) => sum + item.amount, 0)
|
||||
);
|
||||
|
||||
// 近 7 天(含今天,index 6 = 今天)每日收益
|
||||
const weeklyTrend = [0, 0, 0, 0, 0, 0, 0];
|
||||
const todayStart = now.startOf("day");
|
||||
for (const item of completed) {
|
||||
if (!item.time) continue;
|
||||
const diff = todayStart.diff(dayjs(item.time).startOf("day"), "day");
|
||||
if (diff >= 0 && diff < 7) weeklyTrend[6 - diff] += item.amount;
|
||||
}
|
||||
|
||||
const transactions = completed
|
||||
.filter(item => item.time)
|
||||
.sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime())
|
||||
.slice(0, 20)
|
||||
.map(item => {
|
||||
const meta = ORDER_SOURCE_META[item.source];
|
||||
return {
|
||||
id: `${item.source}-${item.rowId}`,
|
||||
type: "earning",
|
||||
title: `任务 ID: #${item.jobId}`,
|
||||
subtitle: `${dayjs(item.time).format("YYYY年MM月DD日")} · ${meta.name}`,
|
||||
amount: this.toMoney(item.amount),
|
||||
status: "settled",
|
||||
icon: meta.icon
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取成功",
|
||||
data: {
|
||||
totalBalance,
|
||||
withdrawableAmount: totalBalance,
|
||||
monthlyEarnings,
|
||||
weeklyTrend: weeklyTrend.map(v => this.toMoney(v)),
|
||||
transactions
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ================= 订单通用辅助 =================
|
||||
|
||||
// 统一订单视图(前端已按此字段名对接,勿改)
|
||||
private toOrderView(row: any, source: string): any {
|
||||
const customer = row.customer || {};
|
||||
const customerName = customer.customerName || "";
|
||||
const base = { id: `${source}-${row.id}`, source, status: Number(row.status ?? 0) };
|
||||
|
||||
if (source === "repair") {
|
||||
const content = row.repairContent || "";
|
||||
const laborFee = this.toMoney(row.laborFee);
|
||||
const materialFee = this.toMoney(row.materialFee);
|
||||
const title = content ? this.truncateText(content, TITLE_MAX) : `${customerName} · 报修`;
|
||||
return {
|
||||
...base,
|
||||
jobId: row.repairNo || "",
|
||||
title,
|
||||
type: ORDER_SOURCE_META.repair.type,
|
||||
amount: this.toMoney(laborFee + materialFee),
|
||||
distance: this.toNumber(row.distance),
|
||||
schedule: row.requestTime ? dayjs(row.requestTime).format("MM-DD HH:mm") : "立即处理",
|
||||
description: content || title,
|
||||
customerName,
|
||||
customerPhone: row.requesterPhone || customer.contactPhone || "",
|
||||
photos: this.toPhotoArray(row.photos),
|
||||
address: row.location || customer.address || "",
|
||||
laborFee,
|
||||
materialFee,
|
||||
estimatedDuration: row.estimatedDuration || ""
|
||||
};
|
||||
}
|
||||
|
||||
if (source === "survey") {
|
||||
const taskTitle = row.taskTitle || "";
|
||||
const remark = row.remark || "";
|
||||
const title = taskTitle || `${customerName} · 勘察`;
|
||||
const amount = row.actualFee !== null && row.actualFee !== undefined ? row.actualFee : row.estimatedFee;
|
||||
return {
|
||||
...base,
|
||||
jobId: row.surveyNo || "",
|
||||
title,
|
||||
type: ORDER_SOURCE_META.survey.type,
|
||||
amount: this.toMoney(amount ?? 0),
|
||||
distance: this.toNumber(row.distance),
|
||||
schedule: row.planStart ? dayjs(row.planStart).format("MM-DD HH:mm") : "预约灵活",
|
||||
description: remark || taskTitle || title,
|
||||
customerName,
|
||||
customerPhone: row.techContact || customer.contactPhone || "",
|
||||
// 勘察 photos 为 JSON 数组字符串,需容错解析
|
||||
photos: this.toPhotoArray(row.photos),
|
||||
address: row.siteLocation || customer.address || "",
|
||||
laborFee: 0,
|
||||
materialFee: 0,
|
||||
estimatedDuration: row.estimatedDuration || ""
|
||||
};
|
||||
}
|
||||
|
||||
// install / maintenance
|
||||
const bizName = source === "install" ? "安装" : "维保";
|
||||
const laborFee = this.toMoney(row.laborFee);
|
||||
const materialFee = this.toMoney(row.materialFee);
|
||||
const scheduleValue = source === "install" ? row.dispatchDate : row.maintenanceDate;
|
||||
return {
|
||||
...base,
|
||||
jobId: (source === "install" ? row.installNo : row.maintenanceNo) || "",
|
||||
title: `${customerName} · ${bizName}`,
|
||||
type: ORDER_SOURCE_META[source].type,
|
||||
amount: this.toMoney(laborFee + materialFee),
|
||||
distance: this.toNumber(row.distance),
|
||||
schedule: scheduleValue ? String(scheduleValue).slice(0, 10) : "预约灵活",
|
||||
description: row.remark || `${customerName} · ${bizName}`,
|
||||
customerName,
|
||||
customerPhone: customer.contactPhone || "",
|
||||
photos: [],
|
||||
address: customer.address || "",
|
||||
laborFee,
|
||||
materialFee,
|
||||
estimatedDuration: row.estimatedDuration || ""
|
||||
};
|
||||
}
|
||||
|
||||
// 各来源对应的仓储(返回 any 以避免泛型联合带来的类型噪音)
|
||||
private sourceModel(source: string): any {
|
||||
if (source === "install") return this.installModel;
|
||||
if (source === "maintenance") return this.maintenanceModel;
|
||||
if (source === "survey") return this.surveyModel;
|
||||
return this.requestModel;
|
||||
}
|
||||
|
||||
// 订单展示用 include:仅取客户必要字段,避免 N+1 与多余数据
|
||||
private customerInclude(): any {
|
||||
return {
|
||||
model: Customer,
|
||||
as: "customer",
|
||||
attributes: ["id", "customerName", "contactPerson", "contactPhone", "address"]
|
||||
};
|
||||
}
|
||||
|
||||
// 派单号规则:PD + YYYYMMDD + 4 位当日序号(北京时间),冲突重试 1 次
|
||||
private async generateDispatchNo(t: any, retry: number = 0): Promise<string> {
|
||||
const prefix = `PD${dayjs().add(8, "hour").format("YYYYMMDD")}`;
|
||||
const last = await this.dispatchModel.findOne({
|
||||
where: { dispatchNo: { [Op.like]: `${prefix}%` } },
|
||||
order: [["dispatch_no", "DESC"]],
|
||||
transaction: t
|
||||
});
|
||||
let seq = 1;
|
||||
if (last) seq = (parseInt(last.dispatchNo.slice(-4), 10) || 0) + 1;
|
||||
const result = `${prefix}${String(seq).padStart(4, "0")}`;
|
||||
const exists = await this.dispatchModel.findOne({ where: { dispatchNo: result }, transaction: t });
|
||||
if (exists) {
|
||||
if (retry < 1) return this.generateDispatchNo(t, retry + 1);
|
||||
throw new Error("派单号生成冲突,请重试");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 完成数 +1(仅在未完成 → 完成的转换时调用)
|
||||
private async bumpCompletedCount(workerId: number, t: any): Promise<void> {
|
||||
await this.workerModel.increment("completedCount", { by: 1, where: { id: workerId }, transaction: t });
|
||||
}
|
||||
|
||||
// 抽取订单创建时间(新老字段兼容),用于合并排序
|
||||
private createdTime(row: any): number {
|
||||
const raw = row?.created_at ?? row?.createdAt;
|
||||
const time = raw ? new Date(raw).getTime() : 0;
|
||||
return Number.isNaN(time) ? 0 : time;
|
||||
}
|
||||
|
||||
// 金额统一保留 2 位小数并转 number(DECIMAL 经 Sequelize 返回字符串)
|
||||
private toMoney(value: any): number {
|
||||
const num = Number(value);
|
||||
return Number.isFinite(num) ? Number(num.toFixed(2)) : 0;
|
||||
}
|
||||
|
||||
// 距离等数值字段统一转 number,非法值兜底 0,保证接口不返回 null/NaN
|
||||
private toNumber(value: any): number {
|
||||
const num = Number(value);
|
||||
return Number.isFinite(num) ? num : 0;
|
||||
}
|
||||
|
||||
// 照片字段容错:JSONB 直接为数组;勘察为 JSON 数组字符串需解析
|
||||
private toPhotoArray(raw: any): string[] {
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return raw;
|
||||
try {
|
||||
const arr = JSON.parse(raw);
|
||||
return Array.isArray(arr) ? arr : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 标题超长截断
|
||||
private truncateText(text: string, max: number): string {
|
||||
if (!text) return "";
|
||||
return text.length > max ? text.slice(0, max) : text;
|
||||
}
|
||||
|
||||
// 查询某电工的全部证照
|
||||
private async listCertRows(workerId: number): Promise<WorkerCert[]> {
|
||||
return await this.certModel.findAll({
|
||||
@@ -289,6 +774,7 @@ export class WorkerAppService {
|
||||
}
|
||||
|
||||
// 电工基础信息(不含 passwordHash)
|
||||
// 注意:rating 为 DECIMAL,Sequelize 返回字符串,必须转 number 供前端直接运算/展示
|
||||
private basicInfo(worker: Worker): any {
|
||||
return {
|
||||
id: worker.id,
|
||||
@@ -296,7 +782,9 @@ export class WorkerAppService {
|
||||
phone: worker.phone,
|
||||
workerTitle: worker.workerTitle,
|
||||
auditStatus: worker.auditStatus,
|
||||
status: worker.status
|
||||
status: worker.status,
|
||||
rating: worker.rating === null || worker.rating === undefined ? 5.0 : Number(worker.rating),
|
||||
completedCount: worker.completedCount === null || worker.completedCount === undefined ? 0 : Number(worker.completedCount)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user