chore(P14-8): 完成运营化与验收收口
这个提交包含在:
+20
-16
@@ -77,9 +77,9 @@ async function buildApp() {
|
||||
|
||||
const allowedOrigins = config.appOrigin
|
||||
? config.appOrigin
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: null;
|
||||
|
||||
await app.register(cors, {
|
||||
@@ -187,6 +187,10 @@ async function buildApp() {
|
||||
app.log.info("OpenAPI docs enabled at /api/docs");
|
||||
}
|
||||
|
||||
if (config.otelTracesEnabled) {
|
||||
app.log.info("OTel tracing enabled");
|
||||
}
|
||||
|
||||
app.get(
|
||||
"/health",
|
||||
{
|
||||
@@ -233,19 +237,19 @@ async function buildApp() {
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
try {
|
||||
await app.prisma.$queryRaw`SELECT 1`;
|
||||
return reply.send({ requestId: request.id, data: { ok: true } });
|
||||
} catch (error) {
|
||||
app.log.error({ err: error, requestId: request.id }, "Readiness check failed");
|
||||
throw new AppError({
|
||||
code: "SYS_503",
|
||||
message: "Service Unavailable",
|
||||
statusCode: 503,
|
||||
details: { db: "unreachable" },
|
||||
});
|
||||
}
|
||||
},
|
||||
try {
|
||||
await app.prisma.$queryRaw`SELECT 1`;
|
||||
return reply.send({ requestId: request.id, data: { ok: true } });
|
||||
} catch (error) {
|
||||
app.log.error({ err: error, requestId: request.id }, "Readiness check failed");
|
||||
throw new AppError({
|
||||
code: "SYS_503",
|
||||
message: "Service Unavailable",
|
||||
statusCode: 503,
|
||||
details: { db: "unreachable" },
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (config.metricsEnabled) {
|
||||
|
||||
@@ -132,6 +132,8 @@ function getConfig() {
|
||||
smtpSecure: process.env.SMTP_SECURE === "true",
|
||||
smtpUser: process.env.SMTP_USER ?? "",
|
||||
smtpPass: process.env.SMTP_PASS ?? "",
|
||||
|
||||
otelTracesEnabled: process.env.OTEL_TRACES_ENABLED === "true",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
const { getConfig } = require("./config");
|
||||
const { initTracing } = require("./observability/tracing");
|
||||
|
||||
const config = getConfig();
|
||||
initTracing(config);
|
||||
|
||||
const { buildApp } = require("./app");
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
const { NodeSDK } = require('@opentelemetry/sdk-node');
|
||||
const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-node');
|
||||
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
|
||||
const { Resource } = require('@opentelemetry/resources');
|
||||
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
|
||||
|
||||
let sdk = null;
|
||||
|
||||
function initTracing(config) {
|
||||
if (config.otelTracesEnabled !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exporter = new ConsoleSpanExporter();
|
||||
|
||||
sdk = new NodeSDK({
|
||||
resource: new Resource({
|
||||
[SemanticResourceAttributes.SERVICE_NAME]: 'hyb-oa-server',
|
||||
}),
|
||||
traceExporter: exporter,
|
||||
instrumentations: [getNodeAutoInstrumentations({
|
||||
'@opentelemetry/instrumentation-fs': { enabled: false },
|
||||
})]
|
||||
});
|
||||
|
||||
try {
|
||||
sdk.start();
|
||||
console.log('OTel tracing initialized');
|
||||
} catch (error) {
|
||||
console.error('Error initializing OTel tracing', error);
|
||||
}
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
if (sdk) {
|
||||
sdk.shutdown()
|
||||
.then(() => console.log('OTel tracing terminated'))
|
||||
.catch((error) => console.log('Error terminating OTel tracing', error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { initTracing };
|
||||
@@ -103,6 +103,7 @@ function registerInvoicePoolReadRoutes(fastify) {
|
||||
fastify.get(
|
||||
"/invoice-pool",
|
||||
{
|
||||
schema: { tags: ["Invoice Pool"], summary: "List Invoices" },
|
||||
preHandler: fastify.authenticate,
|
||||
},
|
||||
async (request, reply) => {
|
||||
@@ -134,19 +135,19 @@ function registerInvoicePoolReadRoutes(fastify) {
|
||||
...(ownerScope === "all"
|
||||
? {}
|
||||
: {
|
||||
OR: [
|
||||
{ owner_employee_id: request.employee.id },
|
||||
{ uploaded_by_employee_id: request.employee.id },
|
||||
{
|
||||
linked_reimbursement: {
|
||||
OR: [
|
||||
{ owner_id: request.employee.id },
|
||||
{ submitter_id: request.employee.id },
|
||||
],
|
||||
},
|
||||
OR: [
|
||||
{ owner_employee_id: request.employee.id },
|
||||
{ uploaded_by_employee_id: request.employee.id },
|
||||
{
|
||||
linked_reimbursement: {
|
||||
OR: [
|
||||
{ owner_id: request.employee.id },
|
||||
{ submitter_id: request.employee.id },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
@@ -173,6 +174,7 @@ function registerInvoicePoolReadRoutes(fastify) {
|
||||
fastify.get(
|
||||
"/invoice-pool/:id",
|
||||
{
|
||||
schema: { tags: ["Invoice Pool"], summary: "Get Invoice Detail" },
|
||||
preHandler: fastify.authenticate,
|
||||
},
|
||||
async (request, reply) => {
|
||||
|
||||
@@ -251,17 +251,17 @@ function mapReimbursementSummary(item) {
|
||||
},
|
||||
owner: item.owner
|
||||
? {
|
||||
id: item.owner.id,
|
||||
employeeNo: item.owner.employee_no,
|
||||
name: item.owner.name,
|
||||
}
|
||||
id: item.owner.id,
|
||||
employeeNo: item.owner.employee_no,
|
||||
name: item.owner.name,
|
||||
}
|
||||
: null,
|
||||
submitter: item.submitter
|
||||
? {
|
||||
id: item.submitter.id,
|
||||
employeeNo: item.submitter.employee_no,
|
||||
name: item.submitter.name,
|
||||
}
|
||||
id: item.submitter.id,
|
||||
employeeNo: item.submitter.employee_no,
|
||||
name: item.submitter.name,
|
||||
}
|
||||
: null,
|
||||
createdAt: item.created_at?.toISOString?.() ?? null,
|
||||
updatedAt: item.updated_at?.toISOString?.() ?? null,
|
||||
@@ -403,6 +403,7 @@ async function reimbursementRoutes(fastify) {
|
||||
fastify.post(
|
||||
"/reimbursements",
|
||||
{
|
||||
schema: { tags: ["Reimbursements"], summary: "Create Reimbursement" },
|
||||
preHandler: fastify.requirePermission("expense.create"),
|
||||
},
|
||||
async (request, reply) => {
|
||||
@@ -526,6 +527,7 @@ async function reimbursementRoutes(fastify) {
|
||||
fastify.get(
|
||||
"/reimbursements",
|
||||
{
|
||||
schema: { tags: ["Reimbursements"], summary: "List Reimbursements" },
|
||||
preHandler: fastify.authenticate,
|
||||
},
|
||||
async (request, reply) => {
|
||||
@@ -602,6 +604,7 @@ async function reimbursementRoutes(fastify) {
|
||||
fastify.get(
|
||||
"/reimbursements/:id",
|
||||
{
|
||||
schema: { tags: ["Reimbursements"], summary: "Get Reimbursement Detail" },
|
||||
preHandler: fastify.authenticate,
|
||||
},
|
||||
async (request, reply) => {
|
||||
@@ -714,12 +717,12 @@ async function reimbursementRoutes(fastify) {
|
||||
description: item.description,
|
||||
expenseType: item.expense_type
|
||||
? {
|
||||
id: item.expense_type.id,
|
||||
code: item.expense_type.code,
|
||||
name: item.expense_type.name,
|
||||
category: item.expense_type.category,
|
||||
enabled: item.expense_type.enabled,
|
||||
}
|
||||
id: item.expense_type.id,
|
||||
code: item.expense_type.code,
|
||||
name: item.expense_type.name,
|
||||
category: item.expense_type.category,
|
||||
enabled: item.expense_type.enabled,
|
||||
}
|
||||
: null,
|
||||
})),
|
||||
budgetHint,
|
||||
@@ -732,10 +735,10 @@ async function reimbursementRoutes(fastify) {
|
||||
})),
|
||||
pendingWorkflow: pendingInstance
|
||||
? {
|
||||
instanceId: pendingInstance.id,
|
||||
stepId: pendingInstance.current_step_id,
|
||||
task: pendingTask,
|
||||
}
|
||||
instanceId: pendingInstance.id,
|
||||
stepId: pendingInstance.current_step_id,
|
||||
task: pendingTask,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
},
|
||||
|
||||
+46
-44
@@ -140,10 +140,10 @@ function isoOrNull(value) {
|
||||
function mapTaskSummary(task) {
|
||||
const workflowInstance = task.workflow_instance
|
||||
? {
|
||||
entity_type: task.workflow_instance.entity_type,
|
||||
entity_id: task.workflow_instance.entity_id,
|
||||
current_status: task.workflow_instance.current_status,
|
||||
}
|
||||
entity_type: task.workflow_instance.entity_type,
|
||||
entity_id: task.workflow_instance.entity_id,
|
||||
current_status: task.workflow_instance.current_status,
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
@@ -168,10 +168,10 @@ function mapTaskSummary(task) {
|
||||
completedAt: isoOrNull(task.completed_at),
|
||||
workflowInstance: workflowInstance
|
||||
? {
|
||||
entityType: workflowInstance.entity_type,
|
||||
entityId: workflowInstance.entity_id,
|
||||
currentStatus: workflowInstance.current_status,
|
||||
}
|
||||
entityType: workflowInstance.entity_type,
|
||||
entityId: workflowInstance.entity_id,
|
||||
currentStatus: workflowInstance.current_status,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
@@ -425,6 +425,7 @@ async function workflowRoutes(fastify) {
|
||||
fastify.get(
|
||||
"/workflow/tasks",
|
||||
{
|
||||
schema: { tags: ["Workflow"], summary: "List Workflow Tasks" },
|
||||
preHandler: fastify.authenticate,
|
||||
},
|
||||
async (request, reply) => {
|
||||
@@ -479,6 +480,7 @@ async function workflowRoutes(fastify) {
|
||||
fastify.get(
|
||||
"/workflow/tasks/:taskId",
|
||||
{
|
||||
schema: { tags: ["Workflow"], summary: "Get Workflow Task Detail" },
|
||||
preHandler: fastify.authenticate,
|
||||
},
|
||||
async (request, reply) => {
|
||||
@@ -612,39 +614,39 @@ async function workflowRoutes(fastify) {
|
||||
},
|
||||
workflowInstance: task.workflow_instance
|
||||
? {
|
||||
id: task.workflow_instance.id,
|
||||
currentStatus: task.workflow_instance.current_status,
|
||||
currentStepId: task.workflow_instance.current_step_id,
|
||||
startedAt: isoOrNull(task.workflow_instance.started_at),
|
||||
completedAt: isoOrNull(task.workflow_instance.completed_at),
|
||||
}
|
||||
id: task.workflow_instance.id,
|
||||
currentStatus: task.workflow_instance.current_status,
|
||||
currentStepId: task.workflow_instance.current_step_id,
|
||||
startedAt: isoOrNull(task.workflow_instance.started_at),
|
||||
completedAt: isoOrNull(task.workflow_instance.completed_at),
|
||||
}
|
||||
: null,
|
||||
reimbursement: task.workflow_instance?.reimbursement_form
|
||||
? {
|
||||
id: task.workflow_instance.reimbursement_form.id,
|
||||
formNumber: task.workflow_instance.reimbursement_form.form_number,
|
||||
status: task.workflow_instance.reimbursement_form.status,
|
||||
reimbursementDate:
|
||||
task.workflow_instance.reimbursement_form.reimbursement_date
|
||||
?.toISOString?.()
|
||||
?.slice(0, 10) ?? null,
|
||||
totalAmount: toMoneyString(task.workflow_instance.reimbursement_form.total_amount),
|
||||
description: task.workflow_instance.reimbursement_form.description,
|
||||
owner: task.workflow_instance.reimbursement_form.owner
|
||||
? {
|
||||
id: task.workflow_instance.reimbursement_form.owner.id,
|
||||
employeeNo: task.workflow_instance.reimbursement_form.owner.employee_no,
|
||||
name: task.workflow_instance.reimbursement_form.owner.name,
|
||||
}
|
||||
: null,
|
||||
submitter: task.workflow_instance.reimbursement_form.submitter
|
||||
? {
|
||||
id: task.workflow_instance.reimbursement_form.submitter.id,
|
||||
employeeNo: task.workflow_instance.reimbursement_form.submitter.employee_no,
|
||||
name: task.workflow_instance.reimbursement_form.submitter.name,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
id: task.workflow_instance.reimbursement_form.id,
|
||||
formNumber: task.workflow_instance.reimbursement_form.form_number,
|
||||
status: task.workflow_instance.reimbursement_form.status,
|
||||
reimbursementDate:
|
||||
task.workflow_instance.reimbursement_form.reimbursement_date
|
||||
?.toISOString?.()
|
||||
?.slice(0, 10) ?? null,
|
||||
totalAmount: toMoneyString(task.workflow_instance.reimbursement_form.total_amount),
|
||||
description: task.workflow_instance.reimbursement_form.description,
|
||||
owner: task.workflow_instance.reimbursement_form.owner
|
||||
? {
|
||||
id: task.workflow_instance.reimbursement_form.owner.id,
|
||||
employeeNo: task.workflow_instance.reimbursement_form.owner.employee_no,
|
||||
name: task.workflow_instance.reimbursement_form.owner.name,
|
||||
}
|
||||
: null,
|
||||
submitter: task.workflow_instance.reimbursement_form.submitter
|
||||
? {
|
||||
id: task.workflow_instance.reimbursement_form.submitter.id,
|
||||
employeeNo: task.workflow_instance.reimbursement_form.submitter.employee_no,
|
||||
name: task.workflow_instance.reimbursement_form.submitter.name,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
: null,
|
||||
allowedActions: listAllowedActionsForTask(request, task, actionable),
|
||||
history: historyItems.map((item) => ({
|
||||
@@ -681,12 +683,12 @@ async function workflowRoutes(fastify) {
|
||||
},
|
||||
...(query.keyword
|
||||
? {
|
||||
OR: [
|
||||
{ name: { contains: query.keyword, mode: "insensitive" } },
|
||||
{ email: { contains: query.keyword, mode: "insensitive" } },
|
||||
{ employee_no: { contains: query.keyword, mode: "insensitive" } },
|
||||
],
|
||||
}
|
||||
OR: [
|
||||
{ name: { contains: query.keyword, mode: "insensitive" } },
|
||||
{ email: { contains: query.keyword, mode: "insensitive" } },
|
||||
{ employee_no: { contains: query.keyword, mode: "insensitive" } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户