Files
cobol-tna-system/JavaSrc/src/Kin08DbuMain.java
T
qiuqiuqiu 8853c7a536 refactor(KIN09/KIN08): align Java to COBOL
- KIN09CSV: FULL outputs daily detail only; SHORT additionally emits monthly summary (matches cbl:160)
- KIN08DBU: RESET deletes MONTHLY_ABSENCE only; DAILY_RECORDS preserved via plain INSERT (matches cbl:486-497)
- Design docs updated to reflect COBOL-correct behavior
- COBOL investigation record: remove the two design self-contradiction items (KIN09 #1, KIN08 #2) and related references
2026-08-30 10:53:24 +08:00

460 lines
18 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import dto.Kin07Rec;
import dto.Kin08Rec;
import dto.ZanendacPar;
import dto.ZanmsgacPar;
import util.ConvUtil;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
/**
* ============================================================
* 【COBOL→Java】KIN08DBU 勤怠DB更新処理
* ============================================================
* PROGRAM-ID. KIN08DBU → 类 Kin08DbuMain
* PGMパターン:DB更新 + SYSIN読込(P28)
* 機能概要:DAILY-RECORD(R01/KIN07REC)を読込み DAILY_RECORDS へ INSERT。
* 社員別月次集計(AGGREGATION-TABLE)後、MONTHLY_ABSENCE へ UPSERT。
* 集計結果を ABSENCE-SUMMARY(W01/KIN08REC) に出力。
* SYSIN制御カード(T/P/M)で対象社員・対象年月・モードを指定。
*/
public class Kin08DbuMain {
private static final String CNS_PRGIDX = "KIN08DBU";
private static final int CNS_MSGSTR = 1;
private static final int CNS_MSGFIN = 2;
private static final int CNS_MSGIINKES = 6;
private static final int CNS_MSGOUTKES = 7;
private static final int CNS_MSGKEYINF = 33;
private static final String CNS_KN0002 = "2";
private static final int CNS_ABD999 = 999;
/** SELECT R01INNFIL ASSIGN TO "KIN08R01". */
private static final String R01_FILE = "data/KIN08R01.DAT";
/** SELECT SYSINFILE ASSIGN TO "KIN08S01". */
private static final String SYSIN_FILE = "data/KIN08S01.DAT";
/** SELECT W01OUTFIL ASSIGN TO "KIN08W01". */
private static final String W01_FILE = "data/KIN08W01.DAT";
// ---- カウンタ ----
private long cunR01Inn;
private long cunDbxIns;
private long cunDbxUpd;
private long cunDbxDel;
private long cunW01Out;
// ---- SYSIN 解析結果 ----
/** Tカード対象社員(指定なし=全社員)。 */
private final List<String> targetList = new ArrayList<>();
private boolean allEmp = true;
/** Pカード対象年月。 */
private String yearMonth = "";
private boolean periodFound = false;
/** Mカード RESET モード。 */
private boolean resetMode = false;
// ---- 集計テーブル(AGGREGATION-TABLE 相当、tenth 単位で保持)----
private static class AggRow {
String empId;
String yearMonth;
int annualH;
int personalH;
int officialH;
int sickH;
int absentH;
/** WS-TOTAL-HOURS(設計書: COMPUTE ROUNDED + ON SIZE ERROR、出力には非含). */
int totalH;
}
private final List<AggRow> aggTable = new ArrayList<>();
private final ZanmsgacPar msgPar = new ZanmsgacPar();
private final ZanendacPar endPar = new ZanendacPar();
private BufferedWriter w01OutFil;
private Connection conn;
public static void main(final String[] args) {
new Kin08DbuMain().run();
}
private void run() {
initProc();
majProc();
stpProc();
}
// *****************************************************************
// 初期処理 1000ITTSOR
// *****************************************************************
private void initProc() {
msgOutPara(CNS_MSGSTR, "", "", "");
msgOutPara(CNS_MSGKEYINF, "COMPILED", "", "");
// ワーク初期化
allEmp = true;
resetMode = false;
// DB接続
try {
Class.forName("org.sqlite.JDBC");
conn = DriverManager.getConnection("jdbc:sqlite:data/kin.db");
conn.setAutoCommit(false);
} catch (Exception e) {
throw new RuntimeException("DB接続失敗: " + e.getMessage(), e);
}
// SYSIN読込
readSysin();
// 必須パラメータ(対象年月)未設定チェック
if (!periodFound) {
abend();
}
// 入出力ファイルOPEN
try {
w01OutFil = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(W01_FILE), StandardCharsets.UTF_8));
} catch (Exception e) {
throw new RuntimeException("出力ファイルOPEN失敗: " + e.getMessage(), e);
}
}
// *****************************************************************
// SYSIN 読込・解析(1199-SYSIN-LOOP / 1110/1120/1130
// *****************************************************************
private void readSysin() {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream(SYSIN_FILE), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
if (line.trim().isEmpty()) {
continue;
}
char card = line.charAt(0);
String body = line.length() > 2 ? line.substring(2) : "";
if (card == 'T') {
// Tカード: 対象社員一覧(カンマ区切り)
targetList.clear();
String[] emps = body.split(",");
for (String e : emps) {
String t = e.trim();
if (!t.isEmpty()) {
targetList.add(t);
}
}
allEmp = false;
} else if (card == 'P') {
// Pカード: YEARMONTH=YYYYMM
String[] kv = body.split("=");
String val = kv.length > 1 ? kv[1].trim() : "";
if (val.length() >= 6) {
yearMonth = val.substring(0, 6);
periodFound = true;
}
} else if (card == 'M') {
// Mカード: MODE=RESET|NORMAL
String mode = body.replace("MODE=", "").trim();
resetMode = "RESET".equals(mode);
} else if (card == '*') {
// コメント行は無視
} else {
System.out.println("WARNING: Unknown card type [" + card + "]");
}
}
} catch (Exception e) {
throw new RuntimeException("SYSIN読込失敗: " + e.getMessage(), e);
}
}
// *****************************************************************
// 主処理 2000MAJSOR
// *****************************************************************
private void majProc() {
// COBOL 通り: RESET は MONTHLY_ABSENCE のみ DELETEDAILY_RECORDS は削除せず通常 INSERT
// ※RESET 再実行時に DAILY_RECORDS 既存行は PK 違反の可能性(COBOL も同様/運用上は事前クリア想定)
if (resetMode) {
resetDelete();
}
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream(R01_FILE), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
if (line.trim().isEmpty()) {
continue;
}
cunR01Inn++;
Kin07Rec rec = new Kin07Rec();
rec.fromRecordLine(line);
// 社員番号有効性チェック(空ならスキップ)
if (rec.getEmpId() == null || rec.getEmpId().trim().isEmpty()) {
System.out.println("WARNING: Empty EMP-ID record " + cunR01Inn);
continue;
}
String empId = rec.getEmpId();
String targetDate = String.format("%08d", rec.getDate());
String timeIn = ConvUtil.padLeftZero(rec.getTimeIn(), 4);
String timeOut = ConvUtil.padLeftZero(rec.getTimeOut(), 4);
double annualH = rec.getAnnualH() / 10.0;
double personalH = rec.getPersonalH() / 10.0;
double officialH = rec.getOfficialH() / 10.0;
double sickH = rec.getSickH() / 10.0;
double absentH = rec.getAbsentH() / 10.0;
// DAILY_RECORDS INSERT
String insSql = "INSERT INTO DAILY_RECORDS "
+ "(EMP_ID, TARGET_DATE, TIME_IN, TIME_OUT, ANNUAL_LEAVE_H, "
+ "PERSONAL_LEAVE_H, OFFICIAL_LEAVE_H, SICK_LEAVE_H, "
+ "UNAPPROVED_ABSENT_H, UPDATED_AT) VALUES "
+ "(?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)";
try (PreparedStatement ps = conn.prepareStatement(insSql)) {
ps.setString(1, empId);
ps.setString(2, targetDate);
ps.setString(3, timeIn);
ps.setString(4, timeOut);
ps.setDouble(5, annualH);
ps.setDouble(6, personalH);
ps.setDouble(7, officialH);
ps.setDouble(8, sickH);
ps.setDouble(9, absentH);
ps.executeUpdate();
cunDbxIns++;
} catch (Exception e) {
dbError(e);
return;
}
// 対象社員フィルタリング(Tカード指定時) → 集計のみスキップ
if (!allEmp && !targetList.contains(empId)) {
continue;
}
// 年月抽出(YYYYMMDD→YYYYMM)
String ym = targetDate.substring(0, 6);
// 集計テーブル加算
AggRow row = findAgg(empId, ym);
if (row == null) {
if (aggTable.size() < 100) {
row = new AggRow();
row.empId = empId;
row.yearMonth = ym;
row.annualH = rec.getAnnualH();
row.personalH = rec.getPersonalH();
row.officialH = rec.getOfficialH();
row.sickH = rec.getSickH();
row.absentH = rec.getAbsentH();
aggTable.add(row);
} else {
System.out.println("WARNING: Agg table full " + empId);
}
} else {
// ANNUAL は COMPUTE ROUNDED(入力は0.1h単位のため ADD と同一結果)、
// 他は ADDV9(1) 切捨だが値は既に0.1h単位)→ 全て単純加算
row.annualH += rec.getAnnualH();
row.personalH += rec.getPersonalH();
row.officialH += rec.getOfficialH();
row.sickH += rec.getSickH();
row.absentH += rec.getAbsentH();
}
// 設計書: WS-TOTAL-HOURS ROUNDED = ANNUAL+PERSONAL+OFFICIAL+SICK+ABSENT
// ON SIZE ERROR → 0 クリア + 警告
if (row != null) {
row.totalH = computeTotalHours(empId, ym, row.annualH, row.personalH,
row.officialH, row.sickH, row.absentH);
}
}
} catch (Exception e) {
throw new RuntimeException("R01読込失敗: " + e.getMessage(), e);
}
}
private AggRow findAgg(final String empId, final String ym) {
for (AggRow r : aggTable) {
if (r.empId.equals(empId) && r.yearMonth.equals(ym)) {
return r;
}
}
return null;
}
/** RESET 時の既存レコード削除(COBOL 通り MONTHLY_ABSENCE のみ。DAILY_RECORDS は削除せず通常 INSERT. */
private void resetDelete() {
try (PreparedStatement ps = conn.prepareStatement(
"DELETE FROM MONTHLY_ABSENCE WHERE YEAR_MONTH = ?")) {
ps.setString(1, yearMonth);
int n = ps.executeUpdate();
cunDbxDel += n;
} catch (Exception e) {
dbError(e);
return;
}
}
/**
* 設計書 2-3: WS-TOTAL-HOURS ROUNDED = ANNUAL+PERSONAL+OFFICIAL+SICK+ABSENT。
* 9(4)V9(1) → tenth 単位上限 99999。溢れ(ON SIZE ERROR)時は 0 クリア+警告。
*/
private int computeTotalHours(final String empId, final String ym,
final int annualH, final int personalH, final int officialH,
final int sickH, final int absentH) {
long sum = (long) annualH + personalH + officialH + sickH + absentH;
if (sum > 99999 || sum < -9999) {
System.out.println("WARNING: Total hours overflow emp=" + empId + " ym=" + ym);
return 0;
}
return (int) sum;
}
// *****************************************************************
// 終了処理 3000STPSOR
// *****************************************************************
private void stpProc() {
try {
// 集計結果 → MONTHLY_ABSENCE UPSERT
for (AggRow row : aggTable) {
String cntSql = "SELECT COUNT(*) FROM MONTHLY_ABSENCE "
+ "WHERE EMP_ID = ? AND YEAR_MONTH = ?";
int cnt;
try (PreparedStatement ps = conn.prepareStatement(cntSql)) {
ps.setString(1, row.empId);
ps.setString(2, row.yearMonth);
try (ResultSet rs = ps.executeQuery()) {
cnt = rs.next() ? rs.getInt(1) : 0;
}
}
if (cnt > 0) {
String updSql = "UPDATE MONTHLY_ABSENCE SET "
+ "ANNUAL_LEAVE_H=?, PERSONAL_LEAVE_H=?, OFFICIAL_LEAVE_H=?, "
+ "SICK_LEAVE_H=?, UNAPPROVED_ABSENT_H=?, UPDATED_AT=CURRENT_TIMESTAMP "
+ "WHERE EMP_ID=? AND YEAR_MONTH=?";
try (PreparedStatement ps = conn.prepareStatement(updSql)) {
ps.setDouble(1, row.annualH / 10.0);
ps.setDouble(2, row.personalH / 10.0);
ps.setDouble(3, row.officialH / 10.0);
ps.setDouble(4, row.sickH / 10.0);
ps.setDouble(5, row.absentH / 10.0);
ps.setString(6, row.empId);
ps.setString(7, row.yearMonth);
ps.executeUpdate();
cunDbxUpd++;
}
} else {
String insSql = "INSERT INTO MONTHLY_ABSENCE "
+ "(EMP_ID, YEAR_MONTH, ANNUAL_LEAVE_H, PERSONAL_LEAVE_H, "
+ "OFFICIAL_LEAVE_H, SICK_LEAVE_H, UNAPPROVED_ABSENT_H, UPDATED_AT) "
+ "VALUES (?,?,?,?,?,?,?,CURRENT_TIMESTAMP)";
try (PreparedStatement ps = conn.prepareStatement(insSql)) {
ps.setString(1, row.empId);
ps.setString(2, row.yearMonth);
ps.setDouble(3, row.annualH / 10.0);
ps.setDouble(4, row.personalH / 10.0);
ps.setDouble(5, row.officialH / 10.0);
ps.setDouble(6, row.sickH / 10.0);
ps.setDouble(7, row.absentH / 10.0);
ps.executeUpdate();
cunDbxIns++;
}
}
}
// ABSENCE_SUMMARY 出力
for (AggRow row : aggTable) {
Kin08Rec out = new Kin08Rec();
out.setEmpId(row.empId);
out.setYearMonth(row.yearMonth);
out.setAnnualH(row.annualH);
out.setPersonalH(row.personalH);
out.setOfficialH(row.officialH);
out.setSickH(row.sickH);
out.setAbsentH(row.absentH);
out.setFiller("");
w01OutFil.write(out.toRecordLine());
w01OutFil.newLine();
cunW01Out++;
}
// COMMIT
conn.commit();
} catch (Exception e) {
dbError(e);
return;
}
// ファイルCLOSE
try {
if (w01OutFil != null) {
w01OutFil.close();
}
} catch (Exception ignore) {
// 無視
}
try {
if (conn != null) {
conn.close();
}
} catch (Exception ignore) {
// 無視
}
// 件数メッセージ
msgOutPara(CNS_MSGIINKES, "KIN08R01", ConvUtil.padLeftZero(cunR01Inn, 9), "");
msgOutPara(CNS_MSGIINKES, "INS", ConvUtil.padLeftZero(cunDbxIns, 9), "");
msgOutPara(CNS_MSGIINKES, "UPD", ConvUtil.padLeftZero(cunDbxUpd, 9), "");
if (cunDbxDel > 0) {
msgOutPara(CNS_MSGOUTKES, "DEL", ConvUtil.padLeftZero(cunDbxDel, 9), "");
}
msgOutPara(CNS_MSGOUTKES, "KIN08W01", ConvUtil.padLeftZero(cunW01Out, 9), "");
msgOutPara(CNS_MSGFIN, "", "", "");
}
// *****************************************************************
// DBエラー処理 9100DBERRSORROLLBACK + ABEND
// *****************************************************************
private void dbError(final Exception e) {
System.out.println("SQL ERROR: " + e.getMessage() + " PGM=KIN08DBU");
try {
if (conn != null) {
conn.rollback();
}
} catch (Exception ignore) {
// 無視
}
abend();
}
/** メッセージ編集出力 4000MSGOUTSORSUB02MSG 呼出). */
private void msgOutPara(final int msgCod, final String p1, final String p2, final String p3) {
msgPar.reset();
msgPar.setM00MsgCod(msgCod);
msgPar.setM00Umkdats2203(CNS_KN0002);
msgPar.setM00Umkdats2204(CNS_KN0002);
msgPar.setM00Umkdats2205(CNS_PRGIDX);
msgPar.setM00Umkdats2201(p1);
msgPar.setM00Umkdats2202(p2);
Sub02MsgSub.execute(msgPar);
}
/** ABEND処理(9999ABDSOR 相当)。 */
private void abend() {
endPar.setE01AbdCod(CNS_ABD999);
Sub03EndSub.execute(endPar);
}
}