diff --git a/book-rent/src/main/java/com/example/demo/service/AuthService.java b/book-rent/src/main/java/com/example/demo/service/AuthService.java index 176ee8a..7c0f92b 100644 --- a/book-rent/src/main/java/com/example/demo/service/AuthService.java +++ b/book-rent/src/main/java/com/example/demo/service/AuthService.java @@ -1,5 +1,34 @@ package com.example.demo.service; -public class AuthService { -} +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import com.example.demo.common.Const; +import com.example.demo.common.LoginUser; +import com.example.demo.entity.Employee; +import com.example.demo.repository.EmployeeRepository; + +/** AUTH-01:認証 */ +@Service +public class AuthService { + private final EmployeeRepository employeeRepository; + + public AuthService(EmployeeRepository employeeRepository) { + this.employeeRepository = employeeRepository; + } + + /** 在籍中の社員のうち、メールアドレスとパスワードが一致する者を返す。一致しなければ empty。 */ + public Optional authenticate(String mailAddress, String password) { + return employeeRepository + .findByMailAddressAndFlgRetirement(mailAddress, Const.RETIRE_NO) + .filter(e -> e.getPassword().equals(password)) + .map(AuthService::toLoginUser); + } + + private static LoginUser toLoginUser(Employee e) { + return new LoginUser(e.getIdEmployee(), e.getNmEmployee(), e.getKnEmployee(), + e.getMailAddress(), e.getFlgAdmin()); + } +} \ No newline at end of file diff --git a/book-rent/src/main/java/com/example/demo/service/BookAdminService.java b/book-rent/src/main/java/com/example/demo/service/BookAdminService.java index 483de2f..17f56d1 100644 --- a/book-rent/src/main/java/com/example/demo/service/BookAdminService.java +++ b/book-rent/src/main/java/com/example/demo/service/BookAdminService.java @@ -1,31 +1,93 @@ package com.example.demo.service; -import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.example.demo.common.BusinessException; +import com.example.demo.common.Const; +import com.example.demo.common.LoginUser; import com.example.demo.entity.BookCollection; -import com.example.demo.entity.BorrowBook; +import com.example.demo.form.BookForm; import com.example.demo.repository.BookCollectionRepository; -import com.example.demo.repository.BorrowBookRepository; + +/** BOOK-02:書籍登録、BOOK-03:書籍編集 */ @Service public class BookAdminService { + private final BookCollectionRepository bookRepository; + + public BookAdminService(BookCollectionRepository bookRepository) { + this.bookRepository = bookRepository; + } + + /** 入力チェック(ISBN必須13文字以内、書籍名必須50、よみ100、出版社50)。※ISBNを数字13桁に限定するかは要確認 */ + public List validate(BookForm f) { + f.trim(); + List e = new ArrayList<>(); + if (f.getIsbn().isEmpty()) e.add("ISBNを入力してください。"); + else if (f.getIsbn().length() > 13) e.add("ISBNは13文字以内で入力してください。"); + if (f.getNmBook().isEmpty()) e.add("書籍名を入力してください。"); + else if (f.getNmBook().length() > 50) e.add("書籍名は50文字以内で入力してください。"); + if (f.getKnBook().length() > 100) e.add("書籍名よみは100文字以内で入力してください。"); + if (f.getPublisher().length() > 50) e.add("出版社は50文字以内で入力してください。"); + return e; + } + + @Transactional(readOnly = true) + public BookCollection find(Integer idBook) { + return bookRepository.findById(idBook) + .orElseThrow(() -> new BusinessException("対象の書籍が見つかりません。")); + } + + public BookForm toForm(BookCollection b) { + BookForm f = new BookForm(); + f.setIdBook(b.getIdBook()); + f.setIsbn(b.getIsbn()); + f.setNmBook(b.getNmBook()); + f.setKnBook(b.getKnBook() == null ? "" : b.getKnBook()); + f.setPublisher(b.getPublisher() == null ? "" : b.getPublisher()); + f.setNote(b.getNote() == null ? "" : b.getNote()); + return f; + } + + /** 新規登録:貸出可として登録する。 */ + @Transactional + public Integer register(BookForm f, LoginUser admin) { + check(f); + BookCollection b = new BookCollection(); + apply(b, f); + b.setFlgLending(Const.LENDING_OK); + b.setIdUpdate(admin.getIdEmployee()); + b.setDateUpdate(LocalDateTime.now()); + return bookRepository.save(b).getIdBook(); + } + + /** 編集:書籍情報のみ更新する(貸出フラグは変更しない)。 */ + @Transactional + public void update(BookForm f, LoginUser admin) { + check(f); + BookCollection b = find(f.getIdBook()); + apply(b, f); + b.setIdUpdate(admin.getIdEmployee()); + b.setDateUpdate(LocalDateTime.now()); + } + + private void check(BookForm f) { + List errors = validate(f); + if (!errors.isEmpty()) throw new BusinessException(String.join(" ", errors)); + } + + + private static void apply(BookCollection b, BookForm f) { + b.setIsbn(f.getIsbn()); + b.setNmBook(f.getNmBook()); + b.setKnBook(f.getKnBook().isEmpty() ? null : f.getKnBook()); + b.setPublisher(f.getPublisher().isEmpty() ? null : f.getPublisher()); + b.setNote(f.getNote().isEmpty() ? null : f.getNote()); + } - BorrowBookRepository borrowBookRepository; - BookCollectionRepository bookCollectionRepository; - - public BookAdminService(BorrowBookRepository borrowBookRepository, BookCollectionRepository bookCollectionRepository) { - this.borrowBookRepository = borrowBookRepository; - this.bookCollectionRepository = bookCollectionRepository; - } - - @Transactional - public void aprroveBookAdmin(BorrowBook idRequest, BookCollection idBook) { - LocalDate today = LocalDate.now(); - - // 貸出テーブルの更新 - BorrowBook brrowBook = borrowBookRepository.findById(idRequest); - - } } diff --git a/book-rent/src/main/java/com/example/demo/service/BookService.java b/book-rent/src/main/java/com/example/demo/service/BookService.java index 725414b..b54cba4 100644 --- a/book-rent/src/main/java/com/example/demo/service/BookService.java +++ b/book-rent/src/main/java/com/example/demo/service/BookService.java @@ -1,5 +1,33 @@ package com.example.demo.service; -public class BookService { +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.demo.common.Const; +import com.example.demo.entity.BookCollection; +import com.example.demo.repository.BookCollectionRepository; + +/** BOOK-01:書籍検索 */ +@Service +public class BookService { + private final BookCollectionRepository bookRepository; + + public BookService(BookCollectionRepository bookRepository) { + this.bookRepository = bookRepository; + } + + /** 貸出可の蔵書を検索する。全て未入力なら全件。 */ + @Transactional(readOnly = true) + public List searchLendable(String nmBook, String publisher, String note) { + return bookRepository.search(Const.LENDING_OK, normalize(nmBook), normalize(publisher), normalize(note)); + } + + /** 前後の空白除去+LIKE特殊文字(! % _)のエスケープ(エスケープ文字は '!') */ + static String normalize(String s) { + if (s == null) return ""; + return s.trim().replace("!", "!!").replace("%", "!%").replace("_", "!_"); + } } + diff --git a/book-rent/src/main/java/com/example/demo/service/EmployeeService.java b/book-rent/src/main/java/com/example/demo/service/EmployeeService.java index 2010b49..0587add 100644 --- a/book-rent/src/main/java/com/example/demo/service/EmployeeService.java +++ b/book-rent/src/main/java/com/example/demo/service/EmployeeService.java @@ -1,5 +1,164 @@ package com.example.demo.service; -public class EmployeeService { +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.demo.common.BusinessException; +import com.example.demo.common.Const; +import com.example.demo.common.LoginUser; +import com.example.demo.entity.Employee; +import com.example.demo.form.EmployeeForm; +import com.example.demo.repository.EmployeeRepository; + +/** EMP-01:社員登録、EMP-02:社員編集 */ +@Service +public class EmployeeService { + private static final Pattern MAIL = Pattern.compile("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"); + private static final String MSG_DUP = "このメールアドレスは既に登録されています。"; + + private final EmployeeRepository employeeRepository; + + public EmployeeService(EmployeeRepository employeeRepository) { + this.employeeRepository = employeeRepository; + } + + // ---------- 検索 ---------- + + @Transactional(readOnly = true) + public Optional findByMail(String mail) { + return employeeRepository.findByMailAddress(mail == null ? "" : mail.trim()); + } + + @Transactional(readOnly = true) + public Employee find(Integer id) { + return employeeRepository.findById(id) + .orElseThrow(() -> new BusinessException("対象の社員が見つかりません。")); + } + + /** 編集画面の初期値(パスワードは空:管理者は変更時のみ入力) */ + public EmployeeForm toForm(Employee e) { + EmployeeForm f = new EmployeeForm(); + f.setIdEmployee(e.getIdEmployee()); + f.setNmEmployee(e.getNmEmployee()); + f.setKnEmployee(e.getKnEmployee()); + f.setMailAddress(e.getMailAddress()); + f.setFlgAdmin(e.getFlgAdmin()); + f.setFlgRetirement(e.getFlgRetirement()); + return f; + } + + // ---------- 入力チェック ---------- + + /** 社員登録:社員名・よみ・メール・管理者フラグ。パスワードは初期値で登録する。 */ + public List validateRegist(EmployeeForm f) { + f.trim(); + List e = new ArrayList<>(); + checkBasic(f, e); + checkMail(f, null, e); + checkFlag(f.getFlgAdmin(), "管理者フラグ", e); + return e; + } + + /** 管理者による編集:全項目。パスワードは空なら変更しない。 */ + public List validateUpdateByAdmin(EmployeeForm f) { + f.trim(); + List e = new ArrayList<>(); + checkBasic(f, e); + checkMail(f, f.getIdEmployee(), e); + checkFlag(f.getFlgAdmin(), "管理者フラグ", e); + checkFlag(f.getFlgRetirement(), "退職フラグ", e); + if (f.getPassword().length() > 256) e.add("パスワードは256文字以内で入力してください。"); + return e; + } + + /** 一般ユーザーによる編集:パスワードのみ(必須)。 */ + public List validateUpdateBySelf(EmployeeForm f) { + f.trim(); + List e = new ArrayList<>(); + if (f.getPassword().isEmpty()) e.add("パスワードを入力してください。"); + else if (f.getPassword().length() > 256) e.add("パスワードは256文字以内で入力してください。"); + return e; + } + + private void checkBasic(EmployeeForm f, List e) { + if (f.getNmEmployee().isEmpty()) e.add("社員名を入力してください。"); + else if (f.getNmEmployee().length() > 50) e.add("社員名は50文字以内で入力してください。"); + // ※社員名よみの文字種(ひらがなのみ等)は設計で確定する。現状は文字数のみチェック + if (f.getKnEmployee().isEmpty()) e.add("社員名よみを入力してください。"); + else if (f.getKnEmployee().length() > 100) e.add("社員名よみは100文字以内で入力してください。"); + } + + private void checkMail(EmployeeForm f, Integer selfId, List e) { + String m = f.getMailAddress(); + if (m.isEmpty()) { e.add("メールアドレスを入力してください。"); return; } + if (m.length() > 256) { e.add("メールアドレスは256文字以内で入力してください。"); return; } + if (!MAIL.matcher(m).matches()) { e.add("メールアドレスの形式が正しくありません。"); return; } + Optional dup = employeeRepository.findByMailAddress(m); + if (dup.isPresent() && !dup.get().getIdEmployee().equals(selfId)) e.add(MSG_DUP); + } + + private static void checkFlag(String v, String label, List e) { + if (!"0".equals(v) && !"1".equals(v)) e.add(label + "が正しくありません。"); + } + + // ---------- 登録・更新 ---------- + + @Transactional + public Integer register(EmployeeForm f, LoginUser admin) { + raise(validateRegist(f)); + Employee e = new Employee(); + e.setNmEmployee(f.getNmEmployee()); + e.setKnEmployee(f.getKnEmployee()); + e.setMailAddress(f.getMailAddress()); + e.setPassword(Const.DEFAULT_PASSWORD); + e.setFlgAdmin(f.getFlgAdmin()); + e.setFlgRetirement(Const.RETIRE_NO); + e.setIdUpdate(admin.getIdEmployee()); + e.setDateUpdate(LocalDateTime.now()); + try { + return employeeRepository.saveAndFlush(e).getIdEmployee(); + } catch (DataIntegrityViolationException ex) { + throw new BusinessException(MSG_DUP); // 同時登録によるUNIQUE制約違反 + } + } + + @Transactional + public void updateByAdmin(EmployeeForm f, LoginUser admin) { + raise(validateUpdateByAdmin(f)); + Employee e = find(f.getIdEmployee()); + e.setNmEmployee(f.getNmEmployee()); + e.setKnEmployee(f.getKnEmployee()); + e.setMailAddress(f.getMailAddress()); + if (!f.getPassword().isEmpty()) e.setPassword(f.getPassword()); + e.setFlgAdmin(f.getFlgAdmin()); + e.setFlgRetirement(f.getFlgRetirement()); + e.setIdUpdate(admin.getIdEmployee()); + e.setDateUpdate(LocalDateTime.now()); + try { + employeeRepository.saveAndFlush(e); + } catch (DataIntegrityViolationException ex) { + throw new BusinessException(MSG_DUP); + } + } + + /** 一般ユーザー:自分自身(セッションの社員ID)のパスワードのみ更新する。 */ + @Transactional + public void updateBySelf(EmployeeForm f, LoginUser self) { + raise(validateUpdateBySelf(f)); + Employee e = find(self.getIdEmployee()); + e.setPassword(f.getPassword()); + e.setIdUpdate(self.getIdEmployee()); + e.setDateUpdate(LocalDateTime.now()); + } + + private static void raise(List errors) { + if (!errors.isEmpty()) throw new BusinessException(String.join(" ", errors)); + } } diff --git a/book-rent/src/main/java/com/example/demo/service/LoanService.java b/book-rent/src/main/java/com/example/demo/service/LoanService.java index 458ef1c..5093088 100644 --- a/book-rent/src/main/java/com/example/demo/service/LoanService.java +++ b/book-rent/src/main/java/com/example/demo/service/LoanService.java @@ -1,5 +1,159 @@ package com.example.demo.service; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.demo.common.BusinessException; +import com.example.demo.common.Const; +import com.example.demo.common.LoginUser; +import com.example.demo.dto.BorrowView; +import com.example.demo.entity.BookCollection; +import com.example.demo.entity.BorrowBook; +import com.example.demo.repository.BookCollectionRepository; +import com.example.demo.repository.BorrowBookRepository; + +/** LOAN-01〜05:貸出申請・承認/却下・返却。貸出ステータスと蔵書の貸出フラグは同一トランザクションで更新する。 */ +@Service public class LoanService { + private static final String MSG_STATE = "他の操作で状態が変わっています。最新の状態を確認してください。"; + private final BorrowBookRepository borrowRepository; + private final BookCollectionRepository bookRepository; + + public LoanService(BorrowBookRepository borrowRepository, BookCollectionRepository bookRepository) { + this.borrowRepository = borrowRepository; + this.bookRepository = bookRepository; + } + + // ---------- LOAN-01 貸出申請 ---------- + + /** 貸出可の蔵書を取得する。存在しない/貸出不可なら BusinessException。 */ + @Transactional(readOnly = true) + public BookCollection getLendableBook(Integer idBook) { + return bookRepository.findById(idBook) + .filter(b -> Const.LENDING_OK.equals(b.getFlgLending())) + .orElseThrow(() -> new BusinessException("この書籍は現在貸出できません。書籍一覧を確認してください。")); + } + + /** 貸出日・返却予定日の入力チェック。エラーがなければ空のリスト。 */ + public List validateDates(String dateBorrow, String dateScheduledReturn) { + List errors = new ArrayList<>(); + LocalDate b = parse(dateBorrow); + LocalDate r = parse(dateScheduledReturn); + if (isBlank(dateBorrow)) errors.add("貸出日を入力してください。"); + else if (b == null) errors.add("貸出日の形式が正しくありません。"); + if (isBlank(dateScheduledReturn)) errors.add("返却予定日を入力してください。"); + else if (r == null) errors.add("返却予定日の形式が正しくありません。"); + if (b != null && r != null && r.isBefore(b)) errors.add("返却予定日は貸出日より前の日付にできません。"); + return errors; + } + + public static LocalDate parse(String s) { + if (isBlank(s)) return null; + try { + return LocalDate.parse(s.trim()); + } catch (DateTimeParseException e) { + return null; + } + } + + private static boolean isBlank(String s) { + return s == null || s.trim().isEmpty(); + } + + /** 申請を登録し、蔵書を貸出不可にする(全体で1トランザクション)。 */ + @Transactional + public Integer apply(Integer idBook, String dateBorrow, String dateScheduledReturn, LoginUser user) { + List errors = validateDates(dateBorrow, dateScheduledReturn); + if (!errors.isEmpty()) throw new BusinessException(String.join(" ", errors)); + + LocalDateTime now = LocalDateTime.now(); + // 貸出可 → 貸出不可。0件なら他の人が先に申請済み + int updated = bookRepository.updateLending(idBook, Const.LENDING_OK, Const.LENDING_NG, user.getIdEmployee(), now); + if (updated == 0) throw new BusinessException("この書籍は現在貸出できません。書籍一覧を確認してください。"); + + BorrowBook r = new BorrowBook(); + r.setIdApplicant(user.getIdEmployee()); + r.setIdBook(idBook); + r.setDateRequest(LocalDate.now()); + r.setStatus(Const.STATUS_WAITING); + r.setDateBorrow(parse(dateBorrow)); + r.setDateScheduledReturn(parse(dateScheduledReturn)); + r.setIdUpdate(user.getIdEmployee()); + r.setDateUpdate(now); + return borrowRepository.save(r).getIdRequest(); + } + + // ---------- 一覧・確認画面 ---------- + + @Transactional(readOnly = true) + public List listByStatus(String status) { + return borrowRepository.findViewsByStatus(status); + } + + /** 申請IDで1件取得し、期待するステータスであることを確認する。 */ + @Transactional(readOnly = true) + public BorrowView getView(Integer idRequest, String expectedStatus) { + BorrowView v = borrowRepository.findViewById(idRequest).stream().findFirst() + .orElseThrow(() -> new BusinessException("対象の申請が見つかりません。")); + if (!expectedStatus.equals(v.getStatus())) throw new BusinessException(MSG_STATE); + return v; + } + + // ---------- LOAN-03 承認/却下 ---------- + + @Transactional + public void approve(Integer idRequest, LoginUser admin) { + BorrowBook r = lockWithStatus(idRequest, Const.STATUS_WAITING); + LocalDateTime now = LocalDateTime.now(); + r.setStatus(Const.STATUS_LENDING); + r.setIdApproval(admin.getIdEmployee()); + r.setDateApproval(LocalDate.now()); + r.setIdUpdate(admin.getIdEmployee()); + r.setDateUpdate(now); + // 蔵書の貸出フラグは貸出不可のまま + } + + @Transactional + public void reject(Integer idRequest, LoginUser admin) { + BorrowBook r = lockWithStatus(idRequest, Const.STATUS_WAITING); + LocalDateTime now = LocalDateTime.now(); + r.setStatus(Const.STATUS_REJECTED); + r.setIdUpdate(admin.getIdEmployee()); + r.setDateUpdate(now); + borrowRepository.saveAndFlush(r); + releaseBook(r.getIdBook(), admin, now); + } + + // ---------- LOAN-05 返却 ---------- + + @Transactional + public void returnBook(Integer idRequest, LoginUser admin) { + BorrowBook r = lockWithStatus(idRequest, Const.STATUS_LENDING); + LocalDateTime now = LocalDateTime.now(); + r.setStatus(Const.STATUS_RETURNED); + r.setDateReturn(LocalDate.now()); + r.setIdUpdate(admin.getIdEmployee()); + r.setDateUpdate(now); + borrowRepository.saveAndFlush(r); + releaseBook(r.getIdBook(), admin, now); + } + + private BorrowBook lockWithStatus(Integer idRequest, String expectedStatus) { + BorrowBook r = borrowRepository.findForUpdate(idRequest) + .orElseThrow(() -> new BusinessException("対象の申請が見つかりません。")); + if (!expectedStatus.equals(r.getStatus())) throw new BusinessException(MSG_STATE); + return r; + } + + /** 蔵書を貸出可に戻す。失敗時は例外でトランザクション全体がロールバックされる。 */ + private void releaseBook(Integer idBook, LoginUser admin, LocalDateTime now) { + int updated = bookRepository.updateLending(idBook, Const.LENDING_NG, Const.LENDING_OK, admin.getIdEmployee(), now); + if (updated == 0) throw new BusinessException(MSG_STATE); + } }