galaxy-opc-plugin 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -145,6 +145,62 @@ const FinanceSchema = Type.Union([
145
145
  action: Type.Literal("generate_funding_datapack"),
146
146
  company_id: Type.String({ description: "公司 ID" }),
147
147
  }),
148
+ Type.Object({
149
+ action: Type.Literal("create_payment"),
150
+ company_id: Type.String({ description: "公司 ID" }),
151
+ direction: Type.String({ description: "方向: receivable(应收) 或 payable(应付)" }),
152
+ counterparty: Type.String({ description: "对方单位名称" }),
153
+ amount: Type.Number({ description: "总金额(元)" }),
154
+ due_date: Type.String({ description: "到期日期 (YYYY-MM-DD)" }),
155
+ invoice_id: Type.Optional(Type.String({ description: "关联发票 ID" })),
156
+ contract_id: Type.Optional(Type.String({ description: "关联合同 ID" })),
157
+ category: Type.Optional(Type.String({ description: "类别: service/product/rent/salary/tax" })),
158
+ payment_method: Type.Optional(Type.String({ description: "支付方式: bank_transfer/alipay/wechat/cash" })),
159
+ notes: Type.Optional(Type.String({ description: "备注" })),
160
+ }),
161
+ Type.Object({
162
+ action: Type.Literal("list_payments"),
163
+ company_id: Type.String({ description: "公司 ID" }),
164
+ direction: Type.Optional(Type.String({ description: "按方向筛选: receivable/payable" })),
165
+ status: Type.Optional(Type.String({ description: "按状态筛选: pending/partial/paid/overdue/cancelled" })),
166
+ counterparty: Type.Optional(Type.String({ description: "按对方单位筛选" })),
167
+ }),
168
+ Type.Object({
169
+ action: Type.Literal("update_payment"),
170
+ payment_id: Type.String({ description: "应收应付记录 ID" }),
171
+ paid_amount: Type.Optional(Type.Number({ description: "已付金额(累加到 paid_amount)" })),
172
+ status: Type.Optional(Type.String({ description: "新状态: pending/partial/paid/overdue/cancelled" })),
173
+ paid_date: Type.Optional(Type.String({ description: "实际付款日期 (YYYY-MM-DD)" })),
174
+ notes: Type.Optional(Type.String({ description: "备注" })),
175
+ }),
176
+ Type.Object({
177
+ action: Type.Literal("payment_summary"),
178
+ company_id: Type.String({ description: "公司 ID" }),
179
+ }),
180
+ Type.Object({
181
+ action: Type.Literal("overdue_check"),
182
+ company_id: Type.String({ description: "公司 ID" }),
183
+ }),
184
+ Type.Object({
185
+ action: Type.Literal("generate_collection_notice"),
186
+ payment_id: Type.String({ description: "应收记录 ID" }),
187
+ tone: Type.Optional(Type.String({ description: "语气: polite(礼貌)/firm(坚定)/urgent(紧急),默认 polite" })),
188
+ }),
189
+ Type.Object({
190
+ action: Type.Literal("classify_payment_risk"),
191
+ company_id: Type.String({ description: "公司 ID" }),
192
+ }),
193
+ Type.Object({
194
+ action: Type.Literal("log_payment_received"),
195
+ payment_id: Type.String({ description: "应收记录 ID" }),
196
+ amount: Type.Number({ description: "到账金额" }),
197
+ received_date: Type.Optional(Type.String({ description: "到账日期 (YYYY-MM-DD),默认今天" })),
198
+ auto_create_transaction: Type.Optional(Type.Boolean({ description: "自动创建交易记录,默认 true" })),
199
+ }),
200
+ Type.Object({
201
+ action: Type.Literal("auto_remind_scheduler"),
202
+ company_id: Type.String({ description: "公司 ID" }),
203
+ }),
148
204
  ]);
149
205
 
150
206
  type FinanceParams = Static<typeof FinanceSchema>;
@@ -182,7 +238,11 @@ export function registerFinanceTool(api: OpenClawPluginApi, db: OpcDatabase): vo
182
238
  "generate_balance_sheet(生成资产负债表), generate_income_statement(生成利润表), " +
183
239
  "generate_cashflow_statement(生成现金流量表), calculate_customer_ltv(计算客户生命周期价值), " +
184
240
  "calculate_acquisition_cost(计算获客成本), unit_economics_analysis(单位经济学分析), " +
185
- "generate_funding_datapack(生成融资数据包)",
241
+ "generate_funding_datapack(生成融资数据包), " +
242
+ "create_payment(创建应收应付记录), list_payments(应收应付列表), " +
243
+ "update_payment(更新应收应付), payment_summary(应收应付汇总), overdue_check(逾期检测), " +
244
+ "generate_collection_notice(AI生成催款文案), classify_payment_risk(应收风险分层), " +
245
+ "log_payment_received(一键登记到账并自动记账), auto_remind_scheduler(自动提醒调度)",
186
246
  parameters: FinanceSchema,
187
247
  async execute(_toolCallId, params) {
188
248
  const p = params as FinanceParams;
@@ -977,6 +1037,598 @@ export function registerFinanceTool(api: OpenClawPluginApi, db: OpcDatabase): vo
977
1037
  });
978
1038
  }
979
1039
 
1040
+ case "create_payment": {
1041
+ const id = db.genId();
1042
+ const now = new Date().toISOString();
1043
+ db.execute(
1044
+ `INSERT INTO opc_payments (id, company_id, direction, counterparty, amount, paid_amount, status, due_date, invoice_id, contract_id, category, payment_method, notes, created_at, updated_at)
1045
+ VALUES (?, ?, ?, ?, ?, 0, 'pending', ?, ?, ?, ?, ?, ?, ?, ?)`,
1046
+ id, p.company_id, p.direction, p.counterparty, p.amount,
1047
+ p.due_date,
1048
+ p.invoice_id ?? "",
1049
+ p.contract_id ?? "",
1050
+ p.category ?? "",
1051
+ p.payment_method ?? "",
1052
+ p.notes ?? "",
1053
+ now, now,
1054
+ );
1055
+ const payment = db.queryOne("SELECT * FROM opc_payments WHERE id = ?", id);
1056
+ return json({ ok: true, payment });
1057
+ }
1058
+
1059
+ case "list_payments": {
1060
+ let sql = "SELECT * FROM opc_payments WHERE company_id = ?";
1061
+ const params2: unknown[] = [p.company_id];
1062
+ if (p.direction) { sql += " AND direction = ?"; params2.push(p.direction); }
1063
+ if (p.status) { sql += " AND status = ?"; params2.push(p.status); }
1064
+ if (p.counterparty) { sql += " AND counterparty = ?"; params2.push(p.counterparty); }
1065
+ sql += " ORDER BY due_date DESC, created_at DESC";
1066
+ const payments = db.query(sql, ...params2);
1067
+ return json({ ok: true, payments });
1068
+ }
1069
+
1070
+ case "update_payment": {
1071
+ const existing = db.queryOne("SELECT * FROM opc_payments WHERE id = ?", p.payment_id) as {
1072
+ id: string;
1073
+ amount: number;
1074
+ paid_amount: number;
1075
+ status: string;
1076
+ paid_date: string;
1077
+ notes: string;
1078
+ } | null;
1079
+
1080
+ if (!existing) {
1081
+ return toolError(`应收应付记录 ${p.payment_id} 不存在`);
1082
+ }
1083
+
1084
+ const updates: string[] = [];
1085
+ const params2: unknown[] = [];
1086
+
1087
+ // 累加已付金额
1088
+ if (p.paid_amount !== undefined) {
1089
+ const newPaidAmount = existing.paid_amount + p.paid_amount;
1090
+ updates.push("paid_amount = ?");
1091
+ params2.push(newPaidAmount);
1092
+
1093
+ // 自动更新状态
1094
+ if (!p.status) {
1095
+ if (newPaidAmount >= existing.amount) {
1096
+ updates.push("status = ?");
1097
+ params2.push("paid");
1098
+ } else if (newPaidAmount > 0) {
1099
+ updates.push("status = ?");
1100
+ params2.push("partial");
1101
+ }
1102
+ }
1103
+ }
1104
+
1105
+ if (p.status) {
1106
+ updates.push("status = ?");
1107
+ params2.push(p.status);
1108
+ }
1109
+
1110
+ if (p.paid_date) {
1111
+ updates.push("paid_date = ?");
1112
+ params2.push(p.paid_date);
1113
+ }
1114
+
1115
+ if (p.notes) {
1116
+ updates.push("notes = ?");
1117
+ params2.push(p.notes);
1118
+ }
1119
+
1120
+ updates.push("updated_at = ?");
1121
+ params2.push(new Date().toISOString());
1122
+
1123
+ params2.push(p.payment_id);
1124
+
1125
+ if (updates.length > 0) {
1126
+ db.execute(
1127
+ `UPDATE opc_payments SET ${updates.join(", ")} WHERE id = ?`,
1128
+ ...params2,
1129
+ );
1130
+ }
1131
+
1132
+ const payment = db.queryOne("SELECT * FROM opc_payments WHERE id = ?", p.payment_id);
1133
+ return json({ ok: true, payment });
1134
+ }
1135
+
1136
+ case "payment_summary": {
1137
+ const receivables = db.queryOne(
1138
+ `SELECT
1139
+ COALESCE(SUM(CASE WHEN status IN ('pending', 'partial') THEN amount - paid_amount ELSE 0 END), 0) as pending_receivable,
1140
+ COALESCE(SUM(CASE WHEN status = 'overdue' THEN amount - paid_amount ELSE 0 END), 0) as overdue_receivable,
1141
+ COALESCE(COUNT(CASE WHEN status = 'overdue' THEN 1 END), 0) as overdue_count
1142
+ FROM opc_payments
1143
+ WHERE company_id = ? AND direction = 'receivable'`,
1144
+ p.company_id,
1145
+ ) as { pending_receivable: number; overdue_receivable: number; overdue_count: number };
1146
+
1147
+ const payables = db.queryOne(
1148
+ `SELECT
1149
+ COALESCE(SUM(CASE WHEN status IN ('pending', 'partial') THEN amount - paid_amount ELSE 0 END), 0) as pending_payable,
1150
+ COALESCE(SUM(CASE WHEN status = 'overdue' THEN amount - paid_amount ELSE 0 END), 0) as overdue_payable,
1151
+ COALESCE(COUNT(CASE WHEN status = 'overdue' THEN 1 END), 0) as overdue_count
1152
+ FROM opc_payments
1153
+ WHERE company_id = ? AND direction = 'payable'`,
1154
+ p.company_id,
1155
+ ) as { pending_payable: number; overdue_payable: number; overdue_count: number };
1156
+
1157
+ // 近 7 天到期
1158
+ const today = new Date().toISOString().slice(0, 10);
1159
+ const sevenDaysLater = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
1160
+
1161
+ const dueSoon = db.query(
1162
+ `SELECT * FROM opc_payments
1163
+ WHERE company_id = ? AND status IN ('pending', 'partial')
1164
+ AND due_date >= ? AND due_date <= ?
1165
+ ORDER BY due_date`,
1166
+ p.company_id, today, sevenDaysLater,
1167
+ );
1168
+
1169
+ return json({
1170
+ ok: true,
1171
+ summary: {
1172
+ receivable: {
1173
+ pending_total: receivables.pending_receivable,
1174
+ overdue_total: receivables.overdue_receivable,
1175
+ overdue_count: receivables.overdue_count,
1176
+ },
1177
+ payable: {
1178
+ pending_total: payables.pending_payable,
1179
+ overdue_total: payables.overdue_payable,
1180
+ overdue_count: payables.overdue_count,
1181
+ },
1182
+ due_within_7_days: dueSoon,
1183
+ },
1184
+ });
1185
+ }
1186
+
1187
+ case "overdue_check": {
1188
+ const today = new Date().toISOString().slice(0, 10);
1189
+
1190
+ // 自动标记逾期记录
1191
+ db.execute(
1192
+ `UPDATE opc_payments
1193
+ SET status = 'overdue', updated_at = ?
1194
+ WHERE status IN ('pending', 'partial') AND due_date < ?`,
1195
+ new Date().toISOString(), today,
1196
+ );
1197
+
1198
+ // 查询所有逾期记录
1199
+ const overduePayments = db.query(
1200
+ `SELECT * FROM opc_payments
1201
+ WHERE company_id = ? AND status = 'overdue'
1202
+ ORDER BY due_date`,
1203
+ p.company_id,
1204
+ );
1205
+
1206
+ // 统计逾期信息
1207
+ const overdueReceivable = db.queryOne(
1208
+ `SELECT
1209
+ COALESCE(SUM(amount - paid_amount), 0) as total,
1210
+ COALESCE(COUNT(*), 0) as count
1211
+ FROM opc_payments
1212
+ WHERE company_id = ? AND direction = 'receivable' AND status = 'overdue'`,
1213
+ p.company_id,
1214
+ ) as { total: number; count: number };
1215
+
1216
+ const overduePayable = db.queryOne(
1217
+ `SELECT
1218
+ COALESCE(SUM(amount - paid_amount), 0) as total,
1219
+ COALESCE(COUNT(*), 0) as count
1220
+ FROM opc_payments
1221
+ WHERE company_id = ? AND direction = 'payable' AND status = 'overdue'`,
1222
+ p.company_id,
1223
+ ) as { total: number; count: number };
1224
+
1225
+ return json({
1226
+ ok: true,
1227
+ overdue_check: {
1228
+ receivable: {
1229
+ total: overdueReceivable.total,
1230
+ count: overdueReceivable.count,
1231
+ },
1232
+ payable: {
1233
+ total: overduePayable.total,
1234
+ count: overduePayable.count,
1235
+ },
1236
+ overdue_list: overduePayments,
1237
+ },
1238
+ note: "已自动标记所有逾期记录",
1239
+ });
1240
+ }
1241
+
1242
+ // ═══════════════════════════════════════════════════════
1243
+ // 新增:回款助手增强功能
1244
+ // ═══════════════════════════════════════════════════════
1245
+
1246
+ case "generate_collection_notice": {
1247
+ const payment = db.queryOne(
1248
+ "SELECT * FROM opc_payments WHERE id = ?",
1249
+ p.payment_id,
1250
+ ) as {
1251
+ counterparty: string;
1252
+ amount: number;
1253
+ paid_amount: number;
1254
+ due_date: string;
1255
+ category: string;
1256
+ } | null;
1257
+
1258
+ if (!payment) {
1259
+ return toolError("PAYMENT_NOT_FOUND", `应收记录 ${p.payment_id} 不存在`);
1260
+ }
1261
+
1262
+ // 计算逾期天数
1263
+ const today = new Date();
1264
+ const dueDate = new Date(payment.due_date);
1265
+ const overdueDays = Math.floor((today.getTime() - dueDate.getTime()) / (1000 * 60 * 60 * 24));
1266
+ const remainingAmount = payment.amount - payment.paid_amount;
1267
+
1268
+ // 使用 LLM 生成催款文案
1269
+ const tone = p.tone ?? "polite";
1270
+ const toneMap: Record<string, string> = {
1271
+ polite: "礼貌、友好、委婉",
1272
+ firm: "坚定、专业、明确",
1273
+ urgent: "紧急、严肃、正式",
1274
+ };
1275
+
1276
+ const prompt = `你是一个专业的商务助手。根据以下信息生成一封催款通知:
1277
+
1278
+ 客户信息:
1279
+ - 客户名称:${payment.counterparty || "客户"}
1280
+ - 欠款金额:${remainingAmount} 元
1281
+ - 已付金额:${payment.paid_amount} 元
1282
+ - 到期日期:${payment.due_date}
1283
+ - 逾期天数:${overdueDays > 0 ? overdueDays + " 天" : "未逾期(还有 " + Math.abs(overdueDays) + " 天到期)"}
1284
+ - 款项类型:${payment.category}
1285
+
1286
+ 语气要求:${toneMap[tone]}
1287
+
1288
+ 请生成以下三个版本:
1289
+ 1. **邮件版本**(subject + body,正式完整)
1290
+ 2. **微信版本**(简短、口语化,100字以内)
1291
+ 3. **短信版本**(精炼,70字以内)
1292
+
1293
+ 要求:
1294
+ - 语气${toneMap[tone]},但保持专业
1295
+ - 明确款项金额和到期日
1296
+ - ${overdueDays > 0 ? "提醒逾期情况" : "友情提醒即将到期"}
1297
+ - 提供联系方式("如有疑问请联系我")
1298
+
1299
+ 返回 JSON 格式:
1300
+ {
1301
+ "email": { "subject": "...", "body": "..." },
1302
+ "wechat": "...",
1303
+ "sms": "..."
1304
+ }`;
1305
+
1306
+ try {
1307
+ const response = await api.callLLM({
1308
+ messages: [{ role: "user", content: prompt }],
1309
+ maxTokens: 800,
1310
+ temperature: 0.7,
1311
+ });
1312
+
1313
+ const content = response.content?.[0];
1314
+ let notice;
1315
+
1316
+ if (content?.type === "text") {
1317
+ // 尝试解析 JSON
1318
+ const jsonMatch = content.text.match(/\{[\s\S]*\}/);
1319
+ if (jsonMatch) {
1320
+ notice = JSON.parse(jsonMatch[0]);
1321
+ } else {
1322
+ // 如果没有 JSON,返回原始文本
1323
+ notice = {
1324
+ email: { subject: "款项到期提醒", body: content.text },
1325
+ wechat: content.text.slice(0, 100),
1326
+ sms: content.text.slice(0, 70),
1327
+ };
1328
+ }
1329
+ } else {
1330
+ return toolError("LLM_ERROR", "LLM 返回格式异常");
1331
+ }
1332
+
1333
+ // 更新催款记录
1334
+ const now = new Date().toISOString();
1335
+ const remindCount = (payment as unknown as { remind_count?: number }).remind_count ?? 0;
1336
+ db.exec(
1337
+ "UPDATE opc_payments SET last_remind_date = ?, remind_count = ?, updated_at = ? WHERE id = ?",
1338
+ now.slice(0, 10),
1339
+ remindCount + 1,
1340
+ now,
1341
+ p.payment_id,
1342
+ );
1343
+
1344
+ return json({
1345
+ ok: true,
1346
+ notice,
1347
+ payment_info: {
1348
+ counterparty: payment.counterparty,
1349
+ amount: remainingAmount,
1350
+ due_date: payment.due_date,
1351
+ overdue_days: overdueDays,
1352
+ },
1353
+ message: `✅ 已生成${tone === "polite" ? "礼貌" : tone === "firm" ? "坚定" : "紧急"}催款文案`,
1354
+ });
1355
+ } catch (err) {
1356
+ return toolError("LLM_CALL_FAILED", `LLM 调用失败: ${err instanceof Error ? err.message : String(err)}`);
1357
+ }
1358
+ }
1359
+
1360
+ case "classify_payment_risk": {
1361
+ const today = new Date().toISOString().slice(0, 10);
1362
+
1363
+ // 先更新逾期天数
1364
+ db.exec(
1365
+ `UPDATE opc_payments
1366
+ SET overdue_days = CAST((julianday('${today}') - julianday(due_date)) AS INTEGER)
1367
+ WHERE direction = 'receivable' AND status IN ('pending', 'partial', 'overdue')`,
1368
+ );
1369
+
1370
+ // 更新风险等级
1371
+ db.exec(
1372
+ `UPDATE opc_payments
1373
+ SET risk_level = CASE
1374
+ WHEN overdue_days <= 7 THEN 'normal'
1375
+ WHEN overdue_days BETWEEN 8 AND 30 THEN 'warning'
1376
+ ELSE 'critical'
1377
+ END
1378
+ WHERE direction = 'receivable' AND status IN ('pending', 'partial', 'overdue')`,
1379
+ );
1380
+
1381
+ // 查询各风险等级的应收
1382
+ const riskStats = db.query(
1383
+ `SELECT
1384
+ risk_level,
1385
+ COALESCE(COUNT(*), 0) as count,
1386
+ COALESCE(SUM(amount - paid_amount), 0) as total_amount
1387
+ FROM opc_payments
1388
+ WHERE company_id = ? AND direction = 'receivable' AND status IN ('pending', 'partial', 'overdue')
1389
+ GROUP BY risk_level`,
1390
+ p.company_id,
1391
+ ) as Array<{ risk_level: string; count: number; total_amount: number }>;
1392
+
1393
+ const classification: Record<string, { count: number; amount: number; list: unknown[] }> = {
1394
+ normal: { count: 0, amount: 0, list: [] },
1395
+ warning: { count: 0, amount: 0, list: [] },
1396
+ critical: { count: 0, amount: 0, list: [] },
1397
+ };
1398
+
1399
+ // 获取详细列表
1400
+ for (const stat of riskStats) {
1401
+ const list = db.query(
1402
+ `SELECT * FROM opc_payments
1403
+ WHERE company_id = ? AND risk_level = ?
1404
+ ORDER BY overdue_days DESC`,
1405
+ p.company_id,
1406
+ stat.risk_level,
1407
+ );
1408
+
1409
+ classification[stat.risk_level] = {
1410
+ count: stat.count,
1411
+ amount: stat.total_amount,
1412
+ list,
1413
+ };
1414
+ }
1415
+
1416
+ // 生成建议
1417
+ const suggestions: string[] = [];
1418
+ if (classification.critical.count > 0) {
1419
+ suggestions.push(
1420
+ `⚠️ ${classification.critical.count} 笔严重逾期(>30天),总计 ${classification.critical.amount.toFixed(2)} 元,建议升级催收或法律途径`,
1421
+ );
1422
+ }
1423
+ if (classification.warning.count > 0) {
1424
+ suggestions.push(
1425
+ `💡 ${classification.warning.count} 笔预警逾期(8-30天),总计 ${classification.warning.amount.toFixed(2)} 元,建议电话跟进`,
1426
+ );
1427
+ }
1428
+ if (classification.normal.count > 0) {
1429
+ suggestions.push(
1430
+ `✅ ${classification.normal.count} 笔正常(≤7天),总计 ${classification.normal.amount.toFixed(2)} 元,保持关注`,
1431
+ );
1432
+ }
1433
+
1434
+ return json({
1435
+ ok: true,
1436
+ risk_classification: classification,
1437
+ suggestions,
1438
+ updated_at: today,
1439
+ });
1440
+ }
1441
+
1442
+ case "log_payment_received": {
1443
+ const now = new Date().toISOString();
1444
+ const receivedDate = p.received_date ?? now.slice(0, 10);
1445
+ const autoCreateTransaction = p.auto_create_transaction ?? true;
1446
+
1447
+ // 获取应收记录
1448
+ const payment = db.queryOne(
1449
+ "SELECT * FROM opc_payments WHERE id = ?",
1450
+ p.payment_id,
1451
+ ) as {
1452
+ company_id: string;
1453
+ counterparty: string;
1454
+ amount: number;
1455
+ paid_amount: number;
1456
+ milestone_id: string;
1457
+ category: string;
1458
+ } | null;
1459
+
1460
+ if (!payment) {
1461
+ return toolError("PAYMENT_NOT_FOUND", `应收记录 ${p.payment_id} 不存在`);
1462
+ }
1463
+
1464
+ // 更新已付金额
1465
+ const newPaidAmount = payment.paid_amount + p.amount;
1466
+ const newStatus = newPaidAmount >= payment.amount ? "paid" : "partial";
1467
+
1468
+ db.exec(
1469
+ `UPDATE opc_payments
1470
+ SET paid_amount = ?, paid_date = ?, status = ?, updated_at = ?
1471
+ WHERE id = ?`,
1472
+ newPaidAmount,
1473
+ receivedDate,
1474
+ newStatus,
1475
+ now,
1476
+ p.payment_id,
1477
+ );
1478
+
1479
+ // 自动创建交易记录
1480
+ let transactionId: string | null = null;
1481
+ if (autoCreateTransaction) {
1482
+ transactionId = db.genId();
1483
+ const categoryMap: Record<string, string> = {
1484
+ service: "service_income",
1485
+ product: "product_income",
1486
+ rent: "rent_income",
1487
+ other: "other_income",
1488
+ };
1489
+ const transactionCategory = categoryMap[payment.category] ?? "other_income";
1490
+
1491
+ db.exec(
1492
+ `INSERT INTO opc_transactions (id, company_id, type, category, amount, description, counterparty, transaction_date, created_at)
1493
+ VALUES (?, ?, 'income', ?, ?, ?, ?, ?, ?)`,
1494
+ transactionId,
1495
+ payment.company_id,
1496
+ transactionCategory,
1497
+ p.amount,
1498
+ `${payment.counterparty} 付款`,
1499
+ payment.counterparty,
1500
+ receivedDate,
1501
+ now,
1502
+ );
1503
+ }
1504
+
1505
+ // 如果关联了里程碑,更新里程碑状态
1506
+ if (payment.milestone_id && newStatus === "paid") {
1507
+ db.exec(
1508
+ `UPDATE opc_contract_milestones
1509
+ SET status = 'completed', completed_date = ?, updated_at = ?
1510
+ WHERE id = ?`,
1511
+ receivedDate,
1512
+ now,
1513
+ payment.milestone_id,
1514
+ );
1515
+ }
1516
+
1517
+ return json({
1518
+ ok: true,
1519
+ payment: db.queryOne("SELECT * FROM opc_payments WHERE id = ?", p.payment_id),
1520
+ transaction: transactionId ? db.queryOne("SELECT * FROM opc_transactions WHERE id = ?", transactionId) : null,
1521
+ message: `✅ 已登记到账 ${p.amount} 元${autoCreateTransaction ? ",已自动记账" : ""}${newStatus === "paid" ? ",应收已全额到账" : `,剩余 ${(payment.amount - newPaidAmount).toFixed(2)} 元`}`,
1522
+ });
1523
+ }
1524
+
1525
+ case "auto_remind_scheduler": {
1526
+ const today = new Date().toISOString().slice(0, 10);
1527
+ const in3Days = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
1528
+ const in7Days = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
1529
+
1530
+ // 查询需要提醒的应收记录
1531
+ const reminders: { type: string; payments: unknown[]; message: string }[] = [];
1532
+
1533
+ // 1. 3天内到期的应收
1534
+ const dueSoon3 = db.query(
1535
+ `SELECT * FROM opc_payments
1536
+ WHERE company_id = ? AND direction = 'receivable'
1537
+ AND status = 'pending'
1538
+ AND due_date BETWEEN ? AND ?
1539
+ ORDER BY due_date`,
1540
+ p.company_id,
1541
+ today,
1542
+ in3Days,
1543
+ );
1544
+
1545
+ if (dueSoon3.length > 0) {
1546
+ reminders.push({
1547
+ type: "due_in_3_days",
1548
+ payments: dueSoon3,
1549
+ message: `⏰ ${dueSoon3.length} 笔应收将在 3 天内到期`,
1550
+ });
1551
+ }
1552
+
1553
+ // 2. 7天内到期的应收
1554
+ const dueSoon7 = db.query(
1555
+ `SELECT * FROM opc_payments
1556
+ WHERE company_id = ? AND direction = 'receivable'
1557
+ AND status = 'pending'
1558
+ AND due_date BETWEEN ? AND ?
1559
+ ORDER BY due_date`,
1560
+ p.company_id,
1561
+ in3Days,
1562
+ in7Days,
1563
+ );
1564
+
1565
+ if (dueSoon7.length > 0) {
1566
+ reminders.push({
1567
+ type: "due_in_7_days",
1568
+ payments: dueSoon7,
1569
+ message: `📅 ${dueSoon7.length} 笔应收将在 7 天内到期`,
1570
+ });
1571
+ }
1572
+
1573
+ // 3. 逾期 7 天的应收
1574
+ const overdue7 = db.query(
1575
+ `SELECT * FROM opc_payments
1576
+ WHERE company_id = ? AND direction = 'receivable'
1577
+ AND status = 'overdue' AND overdue_days >= 7 AND overdue_days < 15
1578
+ ORDER BY overdue_days DESC`,
1579
+ p.company_id,
1580
+ );
1581
+
1582
+ if (overdue7.length > 0) {
1583
+ reminders.push({
1584
+ type: "overdue_7_days",
1585
+ payments: overdue7,
1586
+ message: `⚠️ ${overdue7.length} 笔应收逾期 7-14 天,需要跟进`,
1587
+ });
1588
+ }
1589
+
1590
+ // 4. 逾期 15 天的应收
1591
+ const overdue15 = db.query(
1592
+ `SELECT * FROM opc_payments
1593
+ WHERE company_id = ? AND direction = 'receivable'
1594
+ AND status = 'overdue' AND overdue_days >= 15 AND overdue_days < 30
1595
+ ORDER BY overdue_days DESC`,
1596
+ p.company_id,
1597
+ );
1598
+
1599
+ if (overdue15.length > 0) {
1600
+ reminders.push({
1601
+ type: "overdue_15_days",
1602
+ payments: overdue15,
1603
+ message: `🚨 ${overdue15.length} 笔应收逾期 15-29 天,建议电话催收`,
1604
+ });
1605
+ }
1606
+
1607
+ // 5. 逾期 30 天以上的应收
1608
+ const overdue30 = db.query(
1609
+ `SELECT * FROM opc_payments
1610
+ WHERE company_id = ? AND direction = 'receivable'
1611
+ AND status = 'overdue' AND overdue_days >= 30
1612
+ ORDER BY overdue_days DESC`,
1613
+ p.company_id,
1614
+ );
1615
+
1616
+ if (overdue30.length > 0) {
1617
+ reminders.push({
1618
+ type: "overdue_30_days",
1619
+ payments: overdue30,
1620
+ message: `🔴 ${overdue30.length} 笔应收逾期 30 天以上,建议升级催收或法律途径`,
1621
+ });
1622
+ }
1623
+
1624
+ return json({
1625
+ ok: true,
1626
+ reminders,
1627
+ total_count: reminders.reduce((sum, r) => sum + r.payments.length, 0),
1628
+ message: `已生成 ${reminders.length} 类提醒,共 ${reminders.reduce((sum, r) => sum + r.payments.length, 0)} 笔应收需要关注`,
1629
+ });
1630
+ }
1631
+
980
1632
  case "generate_funding_datapack": {
981
1633
  // 获取公司基本信息
982
1634
  const company = db.queryOne(