- 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
460 lines
18 KiB
Java
460 lines
18 KiB
Java
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 のみ DELETE(DAILY_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 と同一結果)、
|
||
// 他は ADD(V9(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エラー処理 9100DBERRSOR(ROLLBACK + 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();
|
||
}
|
||
|
||
/** メッセージ編集出力 4000MSGOUTSOR(SUB02MSG 呼出). */
|
||
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);
|
||
}
|
||
}
|