Java养老系统源码详解:月账单 care_month_bill 与 Quartz 扣费 Job 实践
在线演示 :管理端后台
标签:#Java #Spring Boot #MyBatis #养老系统 #月账单 #Quartz #care_org_config
1. 月账单何时生成、何时扣款
财务问「账单有了没、钱扣了没」------费用管理 · 月账单 对应 CareMonthBillServiceImpl.generate;定时 careMonthBillGenerateJob 月初跑生成,careMonthBillFullDeductJob 在机构扣费日调 executeDeduct。日扣门店另走 careDayFeeDeductJob ,和 计费设置 care_org_config.dayBilling 联动。

图1 月账单列表 · 生成/扣费

图2 扣费流水
2. CareMonthBillGenerateJob(月初生成)
@Component("careMonthBillGenerateJob")
public class CareMonthBillGenerateJob implements JobHandler {
@Override
@TenantJob
public String execute(String param) {
String billMonth = StrUtil.blankToDefault(
CareBillingJobMeta.resolveRuntimeParam(param), YearMonth.now().toString());
CareMonthBillGenerateReqVO req = new CareMonthBillGenerateReqVO();
req.setBillMonth(billMonth);
CareMonthBillActionRespVO result = careMonthBillService.generate(req);
return result.getMessage();
}
}
Cron 建议 0 0 1 1 * ?;param 可传 yyyy-MM 补跑。
3. CareMonthBillServiceImpl.generate(扫生效合同)
public CareMonthBillActionRespVO generate(CareMonthBillGenerateReqVO reqVO) {
contracts = careContractMapper.selectListActive();
for (CareContractDO contract : contracts) {
if (generateOne(contract, ym, billMonth, true)) {
success++;
} else {
skip++;
}
}
int careExpanded = expandCareExecForBillMonth(ym);
return new CareMonthBillActionRespVO(success, skip, fail, message);
}
合同生效时 generateForContractRange 铺首月区间,syncPaymentBill=false 时不重复登记单。
4. CareMonthBillFullDeductJob(扣费日整单扣)
@Component("careMonthBillFullDeductJob")
public class CareMonthBillFullDeductJob implements JobHandler {
@Override
@TenantJob
public String execute(String param) {
if (careOrgConfigService.isDayBilling(rootDeptId)) {
continue;
}
if (!Objects.equals(careOrgConfigService.getMonthDeductMode(rootDeptId),
CareOrgConfigService.MONTH_DEDUCT_FULL)) {
continue;
}
req.setUntilDate(ym.atEndOfMonth());
careMonthBillService.executeDeduct(req);
}
}
billingDayMonth 限制 1~28,避免 2 月无 29 日。
5. Controller 与 curl
curl -X POST "http://localhost:48080/admin-api/care/month-bill/generate" \
-H "Authorization: Bearer {token}" -H "tenant-id: 123" \
-H "Content-Type: application/json" \
-d '{"billMonth":"2026-09"}'
curl -X POST "http://localhost:48080/admin-api/care/month-bill/deduct" \
-H "Authorization: Bearer {token}" -H "tenant-id: 123" \
-d '{"billMonth":"2026-09","untilDate":"2026-09-30","rootDeptId":10}'
补充:日明细与 changeBiz
月账单头在 care_month_bill,日扣在 care_daily_charge;真正扣余额统一走 CareAccountService.changeBiz ,bizType=MONTH_BILL。

图3 机构计费设置 · 日扣/月扣、扣费日 1~28

图4 财务常对照在住人数与应扣笔数
// CareMonthBillServiceImpl.executeDeduct(节选)
for (CareDailyChargeDO row : pendingRows) {
careAccountService.changeBiz(olderId, WALLET_BALANCE, CHANGE_DEDUCT,
row.getAmount(), row.getExpenseCode(), "MONTH_BILL", billId, remark, null);
row.setStatus(DAILY_DONE);
careDailyChargeMapper.updateById(row);
}
@TableName("care_month_bill")
public class CareMonthBillDO extends CareOrgBaseDO {
private String billMonth;
private Long contractId;
private Long olderId;
private BigDecimal amountDue;
private BigDecimal amountPaid;
private Integer status;
}
| Job 名 | 作用 |
|---|---|
careMonthBillGenerateJob |
月初生成账单 |
careMonthBillFullDeductJob |
扣费日整月扣 |
careDayFeeDeductJob |
日扣模式按日入账 |

图5 仅对在住且合同生效老人生成/扣费
6. 踩坑
- Job 无 Bean :Quartz handlerName 须与
@Component名一致。 - 整单扣 skip :日扣院
isDayBilling=true不走 full deduct。 - 无生效合同:generate 抛 MONTH_BILL_NO_ACTIVE_CONTRACT。
- 定时线程 tenant :必须
@TenantJob。
7. CareMonthBillDeductJob(日明细滚动扣)
除整单扣外,部分机构配置 careMonthBillDeductJob 按日滚动扣待扣明细(与 dayBilling 组合以源码为准)。运维在 基础设施 · 定时任务 查看 handlerName:
careMonthBillGenerateJob # 月初生成
careMonthBillFullDeductJob # 扣费日整单
careDayFeeDeductJob # 日扣门店
手动触发示例(jobId 以库 infra_job 为准):
curl -X PUT "http://localhost:48080/admin-api/infra/job/trigger?id=9101" \
-H "Authorization: Bearer {token}" -H "tenant-id: 123"
8. getFlowDetail(对账)
@GetMapping("/flow-detail")
public CommonResult<CareMonthBillFlowDetailRespVO> getFlowDetail(@RequestParam("id") Long id) {
return success(careMonthBillService.getFlowDetail(id));
}
财务按 monthBillId 拉当月账户扣费/退费流水,与 费用管理 · 资金账户 明细交叉核对。
补充:日志与排错顺序
-
浏览器 Network 看
/admin-api返回 code/msg。 -
IDEA 断点 Service 入口,确认 tenant-id 与业务 id。
-
查库 status 与界面一致(勿只改 checkin 不改 bed)。
-
定时任务看 infra_job 日志与 @TenantJob 租户上下文。
curl -X POST -H "tenant-id: 123" -H "Content-Type: application/json"
-d "{"phone":"13900001001","password":"123456","accountType":2}"
http://127.0.0.1:8086/app-api/care/family-auth/login
联调:application-local 与 tenant-id
本地 zhijian-server 联调时,管理端请求统一带头:
yudao:
tenant:
enable: true
spring:
profiles:
active: local
curl -H "Authorization: Bearer {token}" -H "tenant-id: 123" \
"http://localhost:48080/admin-api/care/..."
403 先查角色是否勾选菜单按钮权限;401 查 token 是否过期。多园环境 rootDeptId 与登录部门一致,否则计费/报表可能看到空列表。
模块路径(Maven)
| 模块 | 典型包路径 |
|---|---|
| institution | zhijian-module-institution/.../service/ |
| hub | zhijian-module-hub/.../service/hub/ |
| bi | zhijian-module-bi/.../service/bi/ |
| monitor | zhijian-module-monitor/.../ |
| community | zhijian-module-community/.../ |
| medical | zhijian-module-medical/.../ |
交付裁剪时 zhijian-server/pom.xml 注释未购业态 JAR,并执行 care_maven_delivery_menu_crop.sql 隐藏菜单,避免「有菜单无 API」。
与 hub 中台衔接
老人主档 care_older 在 hub;机构业务 care_checkin / care_contract 在 institution。中台档案页不存 bed_id,床态以 care_bed.status 为准(见 24 篇)。跨模块只通过 olderId、SPI Bridge 或 REST,不在 hub 里直接 join institution 大表。
演示站对照
在线演示:管理端后台 --- 登录后左侧菜单与源码 @PreAuthorize 权限码一致;改 SQL 菜单后需重新登录刷新路由。
Gitee 社区版源码 :https://gitee.com/xinzexingjian-data_0/older_care