feat: Domain Entity 정의 및 DB 스키마 구축 (#8)
Some checks failed
SPMS_API/pipeline/head There was a failure building this commit

Reviewed-on: https://git.ipstein.myds.me/SPMS/SPMS_API/pulls/9
This commit is contained in:
김선규 2026-02-09 04:03:02 +00:00
commit 41c9667e5a
36 changed files with 3001 additions and 623 deletions

8
.gitignore vendored
View File

@ -56,4 +56,10 @@ Dockerfile
# 기타 캐시 파일 # 기타 캐시 파일
**/*.cache **/*.cache
**/*.tmp **/*.tmp
# 프로젝트 문서 및 AI 설정
Documents/
CLAUDE.md
TASKS.md
.mcp.json

Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 KiB

View File

@ -1,219 +0,0 @@
# 1. Branching Model (Git-Flow 방식)
- 각 브랜치는 사용하는 시점에서 생성을 하고 이슈가 해결(PR까지 완료) 된 경우 제거한다.
- 단, main, develop 브랜치는 **절대 삭제하지 않는다**.
## 1.1. 브랜치 명명 규칙
- 형식 : 타입`/#이슈번호-작업요약`
- 규칙
- `/` : 브랜치를 폴더 구조로 묶어준다.
- `-` : 단어 사이를 연결하며, 띄어쓰기는 금지한다.
- `#` : 이슈 추적을 위해 이슈 번호를 명시한다.
- **영문 소문자**만 사용한다.(한글 금지)
- 예시
- feature/#1-admin-login
- fix/#20-jwt-error
- 예외
- main, develop 브랜치의 명칭은 변하지 않는다.
- release 브랜치는 `release/v버전` 의 형식으로 작성한다.
## 1.2. 브랜치 전략표
| 타입 | 의미 | 브랜치 명 예시 | 생성위치 |
| --- | --- | --- | --- |
| main | 배포 가능 최신 상용 상태 | main | - |
| develop | 다음 배포를 위한 통합 브랜치 | develop | - |
| feature | 새로운 기능 개발 | feature/#1-admin-login | develop |
| fix | 개발 중 버그 수정 | fix/#20-jwt-error | develop, release |
| refactor | 기능 변경 없는 코드 개선 | refactor/#13-folder-structure | develop |
| hotfix | [긴급] 운영 서버 버그 수정 | hotfix/#99-prd-db-error | main |
| release | 배포 버전 준비 | release/v1.0.0 | develop |
## 1.3. Branch Life Cycle
### 1.3.1. ***준비 과정***
- main / develop 브랜치 생성 및 초기화
### 1.3.2. ***개발 과정***
1. develop 에서 feature 브랜치 생성해 개발한다. (이슈 생성 필수)
2. 개발 완료 후 devlop으로 **커밋 > PR > 머지**를 수행한다.
3. 개발 중 발견된 버그는 develop에서 fix 브랜치를 생성하여 수정한다. (이슈 생성 필수)
4. 위 과정을 반복하다 배포 스펙이 완성되면 배포 준비 단계로 넘어간다.
### 1.3.3. ***배포 준비 과정***
1. develop 에서 `release/vX.X.X` 브랜치를 생성한다. (Code Freeze 상태)
- 이후 develop 에서는 다음 버전을 위한 개발을 진행해도 된다.
2. QA 중 발견된 버그는 release 브랜치에서 fix 브랜치를 생성해 release에 직접 커밋한다. (이슈 생성 필수)
3. 최종 테스트가 완료되면 배포 단계로 넘어간다.
### 1.3.4. ***배포 과정***
1. 정기 배포:
- release 브랜치를 main 에 머지하고 해당 버전에 대한 태그를 붙인다.
- 배포가 완료되면 release 브랜치를 develop 에 백머지(back-merge)하여 변경 사항을 동기화한다.
- 두 브랜치에 머지가 완료되면 release 브랜치는 **삭제**한다.
2. 긴급 배포
- main 에서 hotfix 브랜치를 생성하여 버그를 수정한다. (이슈 생성 필수)
- 검증 원칙
- hotfix 브랜치는 main 에 머지하기 전에 반드시 **로컬 테스트**와 **스테이징 검증**을 통과해야 한다.
- 검증과정에서 발생하는 수정 사항은 오직 hotfix 브랜치 내부에서만 커밋한다.
- 검증이 완료되기 전까지는 절대 main 브랜치에 PR을 머지하지 않는다.
- 배포 및 동기화
- 검증이 완료되면 main 에 머지하고 해당 버전에 대한 태그를 붙인다. (버전 patch 상승)
- 동시에 develop 에도 백머지(back-merge)를 수행하여 다음 버전 개발에 반영되게 변경 사항을 동기화한다.
- 모든 작업이 완료되면 hotfix 브랜치는 **삭제**한다.
# 2. Commit Message
- 형식: `타입: 설명 (이슈번호)`
- 타입: [2.1. Commit Type](https://www.notion.so/Git-Version-Control-2ed4d57b3bf0805cb29fff784579d427?pvs=21) 참고
- 설명: 언어 상관없이 편하게 내용 작성
- 이슈번호
- 일반: 해당 커밋이 어떠한 이슈의 참조인지 알려준다. (예: feat: 로그인 구현(#1))
- 동작: 해당 커밋이 연결된 이슈의 상황을 변경함 (PR 작성시 확인)
1. Close, Closes, Closed : 해당 커밋을 끝으로 이슈 닫기 (예: feat: 로그인 구현(Closes #1))
2. Fix, Fixes, Fixed : 해당 커밋을 끝으로 버그 수정되어 이슈 닫기 (예: fix: DB 예외 처리(Fixes #21))
3. Resolve, Resolves, Resolved : 무언가 해결이 되어 이슈 닫기 (예: feat: 푸시 구현(Resolves #13))
## 2.1. Commit Type
- 커밋의 메시지는 [Conventional Commits](https://www.conventionalcommits.org/ko/v1.0.0/) 의 규칙을 따른다.
> 커밋 메시지만 보고 소프트웨어의 버전을 올릴 수 있게 하자.
>
- 소프트웨어 버전은 `Major.Minor.Patch` 로 보통 세자리로 구성이 되는데 커밋 메시지만 보고도 세 자리 중 어디를 올려야 하는지에 대한 가이드이다. (초기 배포 시점의 계산이 아니라 배포 후 부터 계산한다.)
### **2.1.1. fix**
- 상황: 로직상의 오류, 오타, 크래시 등을 수정했으나 기능 명세는 변하지 않았다.
- 커밋: `fix: 화면 오타 수정 (Fixes #23)`
- 브랜치
- `hotfix` 브랜치: 이미 배포된 버전을 긴급 수정하는 것이므로 배포 시 `Patch 버전이 상승` 한다.
- `fix` 브랜치: 다음 버전을 출시 하기 위한 안정화 과정이므로 버전 숫자는 변하지 않는다.
- `feature` / `develop` 브랜치: 다음 버전을 출시 하기 위한 개발 과정이므로 버전 숫자는 변하지 않는다.
### **2.1.2. feat**
- 상황: 기획대로 새로운 기능이 코드에 추가되었다.
- 커밋: `feat: 외부 결제 추가 (Closes #34)`
- 브랜치
- `feature` 브랜치: 오직 이 브랜치에서만 사용한다. 당장 버전이 변하지는 않지만 이후 배포가 되면 `Minor 버전 상승` 의 근거가 된다.
### **2.1.3. BREAKING CHANGE**
- 상황: 이전 버전과 완전 호환되지 않는 기술적인 변경이 발생한다.
- 커밋: `feat!: 유저 API 전체 변경` (해당 타입의 뒤에 `!`를 붙인다.)
- 브랜치
- `feature` 브랜치: 다음 버전을 위한 대공사이므로 개발 단계에서 수행한다.
- 다른 브랜치: 선언 불가
- feature > develop > release > main 으로 머지 되면 `Major 버전이 상승` 한다.
- 해당 타입은 다른 타입에 붙어서 사용되므로 독자적으로 사용되지는 않는다.
---
### **2.1.4.** test
- 상황
1. 새로운 테스트 코드 작성할 때
2. 기존 테스트 코드에 문제가 있어 테스트 코드만 수정할 때 (기능의 수정은 이루어지지 않는다.)
3. 테스트 데이터를 수정하거나 리팩토링 할 때
- 커밋: `test: 로그인 API 테스트`
- 브랜치
- 개발 수정 가능한 모든 브랜치 어디서든 사용 가능하다.
### **2.1.5.** chore
- 상황: 소스 코드는 건드리지 않고 빌드 설정, 패키지 업데이트, 라이브러리 설치 등을 수정했다.
- 커밋: `chore: JSON 패키지 업데이트`
- 브랜치
- 개발 수정 가능한 모든 브랜치 어디서든 사용 가능하다.
### **2.1.6.** refactor
- 상황: 기능과 결과는 100% 동일하지만 내부 코드 구조를 개선하거나 성능을 최적화했다.
- 커밋: `refactor: 로그인 화면 개편`
- 브랜치
- `feature` / `refactor` 브랜치: 주로 개발 단계에서 사용한다.
- `fix` 브랜치: 사용은 가능하나 브랜치의 역할이 다르니 최소한으로 사용한다.
- 다른 브랜치: 선언 불가
### **2.1.7.** style
- 상황: 코드의 동작과 전혀 상관없는 띄어쓰기, 줄 바꿈, 들여쓰기 등을 수정했다.
- 커밋: `style: 불필요한 공백 제거`
- 브랜치
- 개발 수정 가능한 모든 브랜치 어디서든 사용 가능하다.
- 표
| **브랜치 (Branch)** | **feat** | **fix** | **BREAKINGCHANGE** | **test / chore / style** | **refactor** | **비고 (Note)** |
|:-----------------:|:-----------------:|:-----------------:|:------------------:|:------------------------:|:-----------------:|:----------------------------------------|
| **main** | ❌ | ❌ | ❌ | ❌ | ❌ | **Merge Only**<br>(직접 커밋 금지) |
| **develop** | ❌ | ❌ | ❌ | ❌ | ❌ | **Merge Only**<br>(직접 커밋 금지) |
| **feature/...** | **⭕**<br>**(주력)** | ⭕<br>(자체수정) | **⭕**<br>**(가능)** | ⭕ | ⭕ | 모든 작업이 가능한 **자유 구역** |
| **fix/...** | ❌ | **⭕**<br>**(주력)** | ❌ | ⭕ | 🔺<br>(최소한) | 기능 추가 절대 금지<br>테스트 보강 가능 |
| **hotfix/...** | ❌ | **⭕**<br>**(주력)** | ❌ | ⭕ | ❌ | **[긴급]** 긴급 수정 및<br>검증용 테스트만 허용 |
| **release/...** | ❌ | **⭕**<br>**(주력)** | ❌ | ⭕ | ❌ | **[배포전]** 안정화 및<br>검증용 테스트만 허용 |
| **refactor/...** | ❌ | ❌ | ❌ | ⭕ | **⭕**<br>**(주력)** | 동작 변경 금지 (fix 불가)<br>기능 추가 금지 (feat 불가) |
---
# 3. Pull Request(PR) Message
- 프로젝트 최상단 폴더에 `.gitea/pull_request_template.md` 혹은 `.github/` 폴더에 저장하면 PR 생성시 자동으로 내용이 채워진다.
- 타이틀
- PR 은 결국 main 이나 develop 브랜치의 커밋이 되기에 타이틀은 [2. Commit Message](https://www.notion.so/Git-Version-Control-2ed4d57b3bf0805cb29fff784579d427?pvs=21)를 따라서 작성한다.
- 형식: `타입: 작업한 커밋들 요약 설명 (이슈번호)`
- 본문
```markdown
## 📋 작업 요약
- {작업 3줄 요약}
## 🔗 관련 이슈 (Related Issues)
Closes #
## 🛠️ 작업 내용 (Changes)
- [ ] {작업 내용 1}
- [ ] {작업 내용 2}
## 📢 리뷰어 참고 사항 (To Reviewers)
- 없을시 비워둠
- {리뷰어 참고 사항}
## ✅ 체크리스트 (Self Checklist)
- [ ] 빌드(Build)가 성공적으로 수행되었는가?
- [ ] 모든 단위 테스트(Unit Test)를 통과하였는가?
- [ ] 불필요한 로그나 주석을 제거하였는가?
- [ ] 컨벤션(Clean Architecture, Naming)을 준수하였는가?
- [ ] 기밀 정보(비밀번호, 키 등)가 하드코딩 되어있지 않은가?
## 📸 스크린샷 / 테스트 로그 (Screenshots/Logs)
- 없을시 비워둠
```
// 테스트 로그 작성
```
```
---
# 4. 태그(Tag)를 붙이는 2가지 타이밍
- 태그(Tag)는 개발자가 코드에 찍는 **최종 승인 도장**으로, 아무때나 찍는 게 아니라 사용자에게 배포되는 버전 번호가 확정되는 순간에만 작성한다.
- 태그는 오직 main 브랜치에 코드가 머지된 직후에만 붙이는 것을 원칙으로 한다.
## 4.1. 정기 배포
- 상황: release/v1.0.0 브랜치에서 모든 QA를 끝내고 main 브랜치에 PR 후 merge 진행
- 버전: v1.0.0 → v1.1.0
- 태그: v1.1.0
## 4.2. 긴급 배포
- 상황: hotfix/… 브랜치에서 main에서 발생한 급한 오류를 해결하고 main 브랜치에 PR 후 merge 진행
- 버전: v1.1.0 → v1.1.1
- 태그: v1.1.1

View File

@ -1,326 +0,0 @@
# 1. 기술 스택 및 환경
| 기술 | 스택 |
|:----------|:------------------------------------|
| Framework | .NET 9.0 / ASP .NET Core |
| Database | MariaDB (Server Version AutoDetect) |
| ORM | Entity Framework Core (Code-First) |
| API Docs | Swagger (OpenAPI) |
| Cache | Redis |
| MQ | RabbitMQ |
| Logging | Serilog |
| Testing | xUnit, Moq |
---
# 2. 코드 컨벤션
## 2.1. 명명 규칙
- PascalCase (대문자 시작): 클래스명, 메서드명, 프로퍼티명, 파일명, public 필드
- camelCase (소문자 시작): 로컬 변수, 매개 변수
- _camelCase (언더바 시작): private 필드
- Interface 는 반드시 접두사 `I` 를 붙인다.
- 비동기 메서드는 접미사 `Async` 를 붙인다.
## 2.2. 문법 규칙
- `var` 사용: 자료형을 명확하게 우변에서 확인 가능할 경우에는 var를 적극 사용한다.
- 리턴 타입이 뭔지 모르는 경우에는 명시를 해준다.
```csharp
var user = new User(); // 가능
var count = GetCount(); // 불가능
int count = GetCount(); // 리턴 타입을 명시해줄 것
```
- 비동기(Asnyc/Await) 동작: I/O 작업(DB, API호출, 파일)은 **무조건 비동기**로 작성한다.
- 비동기 동작을 하는 메서드의 이름 뒤에는 `Asnyc` 를 붙인다.
- void 대신 `Task` 또는 `Task<T>`를 반환한다.
- LINQ: 가독성을 해치지 않는 선에서 적극 사용하되, 복잡한 쿼리는 쿼리 구문이나 분할 작성 할 것
- 주석 및 문서화 (Swagger): API 정의서와의 동기화를 위해 Controller와 DTO의 모든 Public 멤버에는 반드시 XML 주석(`/// <summary>`)을 작성해야 한다.
-
---
# 3. 아키텍쳐 및 개발 표준
## 3.1. 개요
- 의존성의 방향은 항상 외부에서 내부로만 향한다.
- 프로젝트 분리: 프로젝트는 클린 아키텍쳐를 기반으로 하여 물리적으로 4개의 .csproj 로 분리해 참조 규칙을 **강제**한다.
<br><br>
- 클린 아키텍쳐 샘플 이미지
![CleanArchitecture.png](CleanArchitecture.png)
## 3.2. 계층 구조 (Layers)
- 계층의 정의는 내부에서 외부 순서로 정의한다.
### 3.2.1. 계층 (Layers)
#### 1. Domain (Entities, Enterprise Business Rules)
- 역할: 비즈니스의 핵심 개념, 기업 업무 규칙(Entity)을 정의한다.
- 특징: 외부 라이브러리 (DB, Web, 등)에 대한 의존성이 **없어야** 한다. 순수한 C# 클래스로만 구성된다.
- 요소: Entites, Value Objects, Enums, Domain Exceptions, Repository Interfaces (선택)
#### 2. Application (Use cases, Application Business Rules)
- 역할: 애플리케이션의 비즈니스 로직을 처리하고 도메인과 외부를 연결하는 오케스트레이션 담당한다.
- 특징: 도메인 계층에만 의존하며, 인터페이스를 정의하고 구현은 외부 계층(Infra Structure)에 맡긴다.
- 요소: Service Interfaces, DTOs, Mappers, Validators, Service, Implementations (pure 로직)
#### 3. Infra Structure (Interface Adapters)
- 역할: 애플리케이션 계층에서 정의한 인터페이스를 실제로 구현한다. DB, 외부 API, 파일 시스템 등 외부 세계와 통신한다.
- 특징: 애플리케이션과 도메인을 참조하고 다양한 라이브러리를 사용한다.
- 요소: Repository 구현체, DB Context, 외부 API Client, Migrations
#### 4. Presentation (Frameworks & Drivers)
- 역할: 사용자의 요청(HTTP)을 받아 애플리케이션 계층으로 전달하고 결과를 반환한다.
- 특징: 로직을 가지지 않으며 애플리케이션에 의존한다.
- 요소: Controller, Middlewares, Filters, Program.cs (DI 설정)
### 3.2.2. 프로젝트 참조 규칙
- 프로젝트 내에서 참조 설정을 시스템 상에서 해줘서 파일들 간의 물리적인 연결 고리를 룰에 맞게 설정해 줘야 한다.
- Domain: 참조 없음
- Application: Domain
- Infra Structure: Application, Domain
- Presentation: Application, Infra Structure
## 3.3. 개발 상세 가이드
### 3.3.1. API 프로토콜 및 데이터 규격
- 클라이언트와 서버 간의 모든 통신은 아래 정의된 Header와 Body 규격을 준수해야 한다.
#### 1. 요청 규격 (Request)
- 클라이언트는 API 호출 시, HTTP Header에 다음 필수 메타데이터를 포함해야 한다.
- 메타데이터들은 로그 컨텍스트 식별을 위해 평문으로 전송한다.
| 헤더 명 | 설명 및 용도 | 예시 |
| --- |-------------------------------------| --- |
| Content-Type | 전송 데이터 포맷 | application/octet-stream |
| Authorization | JWT 인증 토큰<br>- L2 등급 이상 필수 | Bearer abcdefg… |
| X-API-KEY | 프로젝트 식별 및 데이터 격리를 위한 고유 키 (테넌트 식별자) | spms_api_custom_key |
| X-Request-ID | 트랜잭션 추적 ID<br>- 매요청 마다 고유값 생성<br>- 재사용 금지 | abcdefg-12aa… |
#### 2. 응답 규격 (Response)
- 서버는 비즈니스 로직의 결과와 상관 없이 항상 아래의 공통 JSON포맷으로 응답한다.
- 단, Body의 내용물인 data 필드는 보안 등급에 따라 암호화될 수 있다.
```json
{
"req_id": "abcdefg-12aa…", "result": "success", "code": "0000", "msg": "Response Success MESSAGE", "data": { ... } }
``` - req_id: 요청 헤더의 X-Request-ID를 그대로 반환 (없을 경우 서버 생성), 비동기 응답 식별용
- result: API 호출 결과에 따라 성공(success) / 실패(fail) 여부 반환
- code: API 명세서에 선언된 코드 값 반환 (성공시 0000)
- msg: 현재 동작에 대한 결과 메시지 반환
- data: 현재 동작에 대한 데이터 반환 (E2EE 적용시 암호화된 문자열)
#### 3. 데이터 매핑 원칙
- Entity 노출 금지 (Layer: Domain)
- DB 테이블과 매핑되거나 핵심 로직을 가진 객체로, **절대** Controller 밖으로 노출하지 않는다.
- DTO 필수 (Layer: Application): 데이터 전송을 위한 껍데기 객체
- Controller는 반드시 `Request DTO` 를 받고 `Response DTO` 를 반환해야 한다.
- API 스펙 변경이 도메인 모델에 영향을 주어서는 안되며 그 반대의 경우도 마찬가지이다.
- Body 암호화 정책
- [3.3.8. 보안 정책](#338-데이터-보안-및-암호화)에 의거 **L3**등급 이상의 data 필드는 암호화된다.
### 3.3.2. 비즈니스 로직과 의존성 주입 (DI)
#### 1. 비즈니스 로직 위치
- 단순 데이터 조회 / 저장
- Repository (Layer: Infra Structure)에서 담당하되, 내부 구현 로직이 없어야 한다.
- 업무 규칙 (유효성 검사, 계산, 흐름 제어)
- 반드시 Service (Layer: Application) 또는 Entity (Layer: Domain) 내부에 위치해야 한다.
- Controller
- 요청을 받고 Service를 호출하고 결과를 DTO로 변환하는 역할만 수행한다.
#### 2. 의존성 역전 원칙 (DIP)
- Interface 정의(Layer: Application)
- 특정한 동작에 대한 정의서를 Application 계층에서 선언한다.
- 예: 유저 정보를 저장하는 기능의 필요 = IUserRepository
- 구현(Layer: Infra Structure)
- 해당 동작의 특별한 동작 구현을 Infra Structure 계층에서 구현한다.
- 예: 저장 기능을 EF Core로 구현 = UserRepository
- 주입 (Layer: Presentation)
- Program.cs 에서 DI를 통해 둘을 연결해준다.
- 예: builder.Services.AddScoped<IUserRepository, UserRepository>();
### 3.3.3. 데이터베이스 접근 (EF Core)
1. Code-First
- C# 엔티티 코드가 메인이 되며 Migration 명령어로 DB를 업데이트 한다.
2. Fluent API 사용
- Entity 클래스의 순수성을 위해 [Key], [Table] 과 같은 어트리뷰트 대신 DbContext 내 OnModelCreating 또는 별도의 IEntityTypeConfiguration 파일에서 설정한다.
3. Production 배포 전략
- 개발/스테이징 환경에서는 자동 마이그레이션을 허용하나, 운영(Production) 환경에서는 절대 Database.Migrate() 자동 실행을 금지한다.
4. Script Migration
- 반드시 dotnet ef migrations script 명령어로 SQL 스크립트를 생성하여, DBA 또는 관리자의 검수를 거친 후 수동/CI 파이프라인을 통해 적용한다.
### 3.3.4. 유효성 관리
- FluentValidation 사용: 잦은 if문 대신 `AbstractValidator<T>` 를 상속 받은 별도 클래스로 분리한다.
- 동작: Controller 진입 전 또는 Service 실행 시점에 자동 검증한다.
- 위치: SPMS.Application/Validators
### 3.3.5. 트랜잭션 관리
- 서비스 단위: 하나의 Service 메서드가 하나의 트랜잭션 단위가 된다.
- 원자성 (Atomicity): 원자적인 작업 단위로 모든 작업이 성공적으로 완료되지 않는다면, 즉 하나라도 실패하면 전체 롤백한다.
### 3.3.6. 예외 처리
- Global Handling: 개별 메서드에서 try-catch로 에러를 삼키지 않는다.
- Custom Exception: 비즈니스 로직 에러는 의도적으로 `throw new BusinessExeption(”msg”)`을 발생시킨다
- Middleware: 전역 미들웨어에서 에러를 잡아 공통 포맷으로 변환하여 응답한다.
### 3.3.7. 인증 및 접근 제어
- 시스템 접근 권한은 Who, Where, How often 의 3중 체계로 검증한다.
#### 1. 인증 - JWT
- Stateless 구조를 위해 JWT 방식을 표준으로 한다.
- 구현
- Authorization 헤더에 Bearer Token 을 담아 전송하며 만료 시 Refresh Token 으로 갱신한다.
#### 2. 인가 - 역할 기반 엑세스 제어 (RBAC)
- 사용자의 Role(Admin, Manager, User) 에 따라 API 접근 권한을 엄격히 분리한다.
- 구현
- Controller 상단에 `[Authorize(Roles="...")]` 어트리뷰트를 명시해 코드 레벨에서 권한을 강제한다.
#### 3. 네트워크 접근 제어
- API Key가 탈취되더라도 허용되지 않은 IP에서의 접근을 원천 차단한다.
- 구현
- 모든 API요청 시 미들웨어에서 클라이언트 IP를 추출한다.
- ServiceIP 테이블에 등록된 IP 대역인지 검증하고 불일치 시 403 Forbidden 처리한다.
#### 4. API 속도 제한
- Dos 공격 및 루프로 인한 과부하 방지
- AspNetCoreRateLimit 을 사용하여 IP 주소 또는 API Key 별로 초당/분당 요청 쿼터를 제한한다.(초과시 429 Too Many Requests)
### 3.3.8. 데이터 보안 및 암호화
- 데이터의 생명 주기 (저장, 전송) 전반에 걸쳐 데이터 등급에 따른 암호화 정책을 적용한다.
#### 1. 데이터 등급 분류
| 등급 | 암호화 방식 | 대상 |
| --- | --- | --- |
| L1 (Public) | 평문 | 공지사항, 일반 리소스 |
| L2 (Authenticated) | HTTPS + JWT | 일반 서비스 데이터 |
| L3 (Sensitive) | HTTPS + E2EE + AES-256 저장 | API Key, 토큰 |
| L4 (Critical) | HTTPS + E2EE + BCrypt 저장 | 비밀번호 |
#### 2. 암호화 - 저장
- 양방향 암호화: 외부 연동에 필요한 민감 정보는 AES-256 (CBC/GCM) 알고리즘으로 암호화해 DB에 저장한다.
- 단방향 해시: 비밀번호 등 복호화가 불필요한 정보는 BCrypt 알고리즘(Salt 포함)을 사용해 해싱 저장한다. (절대 평문 저장 금지)
- Key 관리: 암호화 키는 소스 코드가 아닌 환경 변수 또는 Key Vault로 관리한다.
#### 3. 암호화 - 전송
- 적용 대상 : L3, L4 등급의 데이터 전송 시 필수 적용
- 알고리즘: AES-256 (데이터 암호화) + RSA-2048 (키 교환) 하이브리드 방식
- 구현 프로세스
- **[요청 시: Client → Server]**
1. 암호화 값 생성: Client 는 요청마다 새로운 일회용 대칭키(AES Key) 와 IV 를 생성한다.
2. 데이터 암호화: 요청 본문을 생성한 Key와 IV로 암호화 한다.
3. 키 암호화: 생성한 AES Key를 서버의 공개키로 암호화한다. (RSA)
4. 패킹 구조 (Parsing 효율성을 위해 고정 길이 필드 우선 배치)
- JSON 포맷 사용 금지: 페이로드의 효율성과 구조 은닉을 위해 바이트 배열로 직렬화하여 전송한다.
- 구조: `[ Encrypted AES Key (256 Bytes) ] + [ IV (16 Bytes) ] + [ Encrypted Body (Variable) ]`
- Body가 없는 단순 조회 요청이라도 Key와 IV는 **필수 전송**한다.(Body = 0 Byte)
- 파싱 전략
- Server는 수신된 바이너리 스트림의 첫 256바이트를 잘라 RSA 복호화하여 AES Key 획득
- 다음 16바이트를 잘라 IV 획득
- 나머지 바이트를 AES 복호화해 원본 Body 획득
- **[응답 시: Server → Client]**
1. 키 재사용 : 응답 시에는 키 교환을 하지 않고, 요청 패킷에서 획득한 AES Key를 그대로 재사용한다.
2. 새로운 IV 생성: 보안 강화를 위해 응답을 위한 새로운 IV를 새로 생성한다.
3. 패킹 구조 (Parsing 효율성을 위해 고정 길이 필드 우선 배치)
- 구조: `[ IV (16 Bytes) ] + [ Encrypted Body (Variable) ]`
- Client 는 자신이 보냈던 Key와 응답에 포함된 IV 를 사용해 복호화 한다.
4. 복호화: Client는 자신이 갖고 있던 AES Key와 응답 패킷의 IV를 사용해 본문을 복호화한다.
### 3.3.9. 보안 감사 및 시큐어 코딩
- 이미 정의된 네트워크/암호화 보안 외에 Application 계층에서 발생할 수 있는 취약점을 방어하고 추적성을 확보한다.
#### 1. 보안 감사 로그
- 저장소: SystemLog 테이블 (비동기 저장)
- 기록 대상
- 인증 실패: 로그인 n회 연속 실패, 비인가 프로젝트 키 접근 시도
- 권한 위반: 401 Unauthorized, 403 Forbidden 응답 발생한 모든 요청
- 중요한 변경: 관리자에 의한 데이터 변경, 데이터 삭제 행위
- 로그 마스킹
- 헤더 및 개인정보는 로그 저장 시 반드시 마스킹 처리를 한다.
- 비밀번호, 암호화 키 원문은 어떠한 경우에도 로그 파일이나 콘솔에 평문으로 출력하지 않는다.
#### 2. 시큐어 코딩
- SQL Injection 방지: Raw Query 사용을 금지하며 반드시 EF Core(ORM) 메서드나 Parameterized Query 만 사용한다.
- XSS(Cross-Site Scripting) 방지
- BO 에서 HTML 입력을 허용해야 하는 경우 `<script>`, `<iframe>` 등 위험 태그를 제거 후 저장한다. (라이브러리: HtmlSanitizer)
- 그 외 모든 입/출력 데이터는 기본적으로 HTML Encode 처리한다.
### 3.3.10. 웹 보안 및 헤더 정책
- [3.3.1의 요청 규격](#331-api-프로토콜-및-데이터-규격)과 달리 서버가 BO 등 브라우저 클라이언트 보호를 위해 응답시 자동으로 주입하는 보안 헤더 정책이다.
- 미들웨어에서 일괄 처리해서 진행한다.
1. HTTPS 강제 (HSTS)
- 중간자 공격 방지 및 프로토콜 보안 강화
- 설정: Strict-Transport-Security: max-age=31536000; includeSubDomains
2. CORS
- `AllowAnyOrigin(*)` 설정을 **절대 금지**한다.
- 설정: 화이트리스트에 등록된 도메인(Admin URL, Partner URL)의 요청에 대해서만 허용 응답을 반환한다.
3. 보안 헤더 자동 주입
- 모든 API 응답에 아래 헤더를 강제로 포함시켜 브라우저의 보안 기능을 활성화한다.
| 헤더 명 | 방어 목적 | 설정 값 |
| ----------------------- | -------------------------------------- | ------------------- |
| X-Frame-Options | 클릭재킹(Clickjacking) 및 `<iframe>` 임베딩 차단 | DENY |
| X-XSS-Protection | 브라우저 내장 XSS 필터 강제 활성화 | 1; mode=block |
| X-Content-Type-Options | MIME 타입 스니핑 차단 (악성 파일 실행 방지) | nosniff |
| Content-Security-Policy | 승인된 소스 외의 스크립트/리소스 로딩 차단 | default-src 'self'; |
### 3.3.11. 핵심 기술 구현 전략
- 주요 난제인 대량 트래픽 처리와 멀티 테넌시 격리를 위해 아래 4가지를 강제한다.
#### 1. 멀티 테넌시 격리
- Context Caching
- 미들웨어는 X-API-KEY 검증 시 매번 DB를 조회하지 않고, Redis 캐시를 우선 조회하여 ProjectID를 식별한다.
- Global Query Filter
- 식별된 ProjectID를 EF Core의 Global Query Filter에 주입하여, 개발자의 실수로 인한 타 프로젝트 데이터 조회를 원천 차단한다.
#### 2. 멱등성 보장
- 중복 발송 방지
- 네트워크 타임아웃으로 인한 재시도로 중복 실행을 막기 위해, 요청 진입 시 Redis에 X-Request-ID를 키로 조회한다.
- Atomic Check
- 이미 처리 중이거나 완료된 ID라면 작업을 즉시 폐기(Discard)한다
#### 3. 대량 처리 최적화
- Bulk Insert
- 대량의 데이터 저장시 단건 Loop Insert를 금지하며, EF Core Bulk Extensions 를 사용하여 단일 커넥션으로 처리한다.
- MQ QoS (Rabbit MQ)
- Worker 서비스는 RabbitMQ의 Prefetch Count를 적정값으로 설정하여 메모리 과부하 없이 안정적으로 메시지를 소비해야 한다.
#### 4. 데드 토큰 자동 정리
- Worker 서비스가 발송 결과를 처리할 때 APNs/FCM 으로부터 Unregistered(앱 삭제) 또는 BadDeviceToken 응답을 받으면 즉시 해당 토큰을 DB에서 물리 삭제하여 비용 낭비를 방지한다.
### 3.3.12. API 버전 관리 (Versioning Strategy)
- URI Versioning: 모든 API 엔드포인트는 URI 경로에 버전을 명시하여, 클라이언트의 강제 업데이트 없이도 구버전을 지원해야 한다.
- 규칙: `GET /api/v1/users` (메이저 버전만 명시)
- 정책: Breaking Change(필드 삭제, 타입 변경 등)가 발생할 경우에만 v2로 버전을 올리며, 기존 v1은 최소 N개월간 유지(Deprecated) 후 종료한다.
### 3.3.13. 시스템 회복 탄력성 (Resilience & Fault Tolerance)
- 외부 시스템(DB, Redis, FCM, APNs) 장애가 전체 시스템 중단으로 전파되는 것을 막기 위해 Polly 라이브러리를 사용하여 아래 정책을 적용한다.
- Retry (재시도): 일시적인 네트워크 오류 발생 시, Exponential Backoff(지수 대기: 2초, 4초, 8초...) 방식으로 최대 3회 재시도한다.
- Circuit Breaker (차단기): 특정 외부 시스템(예: FCM)이 연속 N회 실패 시, 즉시 회로를 차단(Open)하여 대기 시간 없이 Fail-Fast 처리하고 시스템 리소스를 보호한다.
(일정 시간 후 재접속 시도)
- Timeout: 모든 외부 호출에는 반드시 제한 시간(Timeout)을 설정한다.
### 3.3.14. 상태 모니터링 (Health Checks)
- 로드밸런서 및 모니터링 시스템이 서버의 생존 여부를 판단할 수 있도록 표준 엔드포인트를 제공한다.
- Endpoint: `GET /health`
- 검사 항목
- Liveness: 서버 프로세스가 떠 있는가? (단순 Ping)
- Readiness: DB, Redis, RabbitMQ와 정상적으로 연결되어 트래픽을 받을 준비가 되었는가?
- 응답: 모든 연결 정상 시 200 OK, 하나라도 실패 시 503 Service Unavailable 반환.
## 3.4. 폴더 및 네임스페이스 구조
```
📂 SPMS.Solution
├── 📂 SPMS.Domain
│ ├── Entities (User.cs, Project.cs)
│ ├── Enums (UserRole.cs)
│ └── Exceptions (UserNotFoundException.cs)
├── 📂 SPMS.Application
│ ├── Interfaces (IUserService.cs, IUserRepository.cs)
│ ├── DTOs (Req, Res)
│ ├── Services (UserService.cs - 인터페이스 구현 아님, 로직 담당)
│ └── Mappers (MappingProfile.cs)
├── 📂 SPMS.Infrastructure
│ ├── Persistence (AppDbContext.cs)
│ ├── Repositories (UserRepository.cs - IUserRepository 구현)
│ └── ExternalServices (EmailService.cs)
└── 📂 SPMS.API (ASP.NET Core Web API)
├── Controllers (UsersController.cs)
├── Middlewares
└── Program.cs
```

View File

@ -1,73 +0,0 @@
# 1. 협업 & 작업 프로세스 (Workflow)
## 1.1. Tool Chain
- 문서화 (Documentation)
- Google Documents : [구글 드라이브 링크](https://drive.google.com/drive/folders/1m-2W7BcwHgKuFFNEr9Pblgpce3NEB_TV?usp=drive_link)
- Notion : [노션 SPMS 홈 링크](https://www.notion.so/2eb4d57b3bf08194b974d3384a639860?pvs=21)
- 이슈 관리 (Issue Tracking)
- YouTrack : [YouTrack SPMS 프로젝트 링크](https://ipstein.youtrack.cloud/projects/)
- 코드 관리 (Code Repository)
- Gitea : [Gitea SPMS 프로젝트 링크](https://git.ipstein.myds.me/SPMS)
- Commnication
- Discord
- Slack
## 1.2. Task LifeCycle
- 모든 개발 작업은 YouTrack Issue 생성 없이는 시작할 수 없다.
- 프로젝트 관리는 YouTrack에서, 형상관리는 Gitea에서 수행한다.
### 1. 이슈 등록 (***Require***)
- 작업할 단위 기능을 해당하는 YouTrack에 등록한다. (Gitea Issue 미사용)
#### 요약 (***Require***)
- `작업_제목` (예시: 로그인 API 구현)
- 기존의 `[분류]` 에 해당하는 부분은 `유형` 필드로 대체하므로 적지 않는다.
- 설명 (***Require***)
- 현재 작업하는 내용에 대한 리스트 작성한다.
- 그외의 기획서 링크가 있다면 본문에 첨부한다.
#### 필드 설정 (***Require***)
- 다음 사용자 지정 필드는 반드시 선택한다.
1. 파트 (***Require***): 작업할 서비스를 선택한다.
2. 우선순위 (***Require***): 긴급/ 높음/ 보통/ 낮음/ 없음
3. 유형 (***Require***): Feature(기능 개발)/ Bug(버그)/ Design(디자인)/ Refactor(리팩토링)/ Improvement(기능 개선)/ Chore(설정 관리)/ Documentation(문서 작업)
4. 상태 (***Require***): 대기/ 진행중/ 확인 대기/ 완료
5. Milestones
- 배포 버전 관리 (Gitea의 Milestones 생성 불필요)
- 형식: v1.0.0, MVP, CBT 등
6. Sprints
- 작업 기간 관리
- 형식: 2026-01 Sprint 1 등
7. 담당자
- 개발 담당(복수 선택 가능)을 선택한다.
- 이슈를 생성해줬다면 해당 이슈가 프로젝트에 맞게 애자일 보드에 배치다 잘 되었있는지 확인한다.
### 2. Branch 생성
- 1에서 생성한 YouTrack Issue ID(`SPMS-nn`) 확인후 브랜치 생성한다.
- 형식: `모델/이슈ID-작업요약`
- 모델: Git 전략의 [1. Branching Model (Git-Flow 방식)](./GitFlow.md/#1-branching-model-git-flow-방식) 참고
- 이슈ID: 이슈번호의 생략하지 않고 전부 작성한다.
- 작업 요약: `동사-명사` 형식으로 작성하며 영문으로 작성하고 둘간의 연결은 `-` 로 한다.
- 예: feature/SPMS-1-login-api
### 3. 개발 & Commit
- 로컬에서 작업 후 커밋시, YouTrack 연동을 위해 메시지 규칙을 엄수한다.
- 커밋 메시지의 형태는 [Conventional Commits](https://www.conventionalcommits.org/ko/v1.0.0/) 의 규칙을 따른다.
- Git 전략의 [2. Commit Message](./GitFlow.md/#2-commit-message) 참고
- 단, YouTrack 에 따라서 형식은 `타입: 설명 (이슈ID)` 로 지정한다.
- 예: feat: 로그인 UI 작업 (Close SPMS-1)
### 4. Pull Request (PR)
- 작업 완료 후 develop 브랜치로 PR을 생성한다.
- PR은 피치못한 사정이 있지 않는 한 최소 1명 이상의 리뷰를 거쳐야 한다.
- PR 본문에 작업 내용과 테스트 결과를 요약한다.
- Git 전략의 [3. Pull Request(PR) Message](./GitFlow.md/#3-pull-requestpr-message) 참고
### 5. Merge & Close
- 코드 리뷰 (Self-Review 포함) 후 Merge 한다.
- 자동 완료
- 커밋 메시지에 특정 명령어를 사용했다면 Merge 시 YouTrack 이슈가 자동으로 `해결됨` 상태로 지정된 값 중 최상단 값의 상태로 변경된다.
- 다른 상태로 변경하고 싶다면 이슈 ID 작성 부분에 해당 값의 영어 이름을 넣어준다.
- 대기: Available
- 진행 중: In Progress
- 확인 대기: To Verify
- 완료: Done
- 수동 완료**:** 명령어를 누락했다면, 개발자가 직접 YouTrack에서 상태를 변경해야 한다.
- YouTrack 애자일 보드는 '상태(State)' 필드에 따라 자동으로 카드가 이동하므로, 별도의 보드 관리가 필요 없다.

View File

@ -0,0 +1,17 @@
namespace SPMS.Domain.Entities;
public class Admin : BaseEntity
{
public string AdminCode { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public byte Role { get; set; }
public bool EmailVerified { get; set; }
public DateTime? EmailVerifiedAt { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? LastLoginAt { get; set; }
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
}

View File

@ -0,0 +1,6 @@
namespace SPMS.Domain.Entities;
public abstract class BaseEntity
{
public long Id { get; set; }
}

View File

@ -0,0 +1,15 @@
namespace SPMS.Domain.Entities;
public class DailyStat : BaseEntity
{
public long ServiceId { get; set; }
public DateOnly StatDate { get; set; }
public int SentCnt { get; set; }
public int SuccessCnt { get; set; }
public int FailCnt { get; set; }
public int OpenCnt { get; set; }
public DateTime CreatedAt { get; set; }
// Navigation
public Service Service { get; set; } = null!;
}

View File

@ -0,0 +1,22 @@
namespace SPMS.Domain.Entities;
public class Device : BaseEntity
{
public long ServiceId { get; set; }
public string DeviceToken { get; set; } = string.Empty;
public byte Platform { get; set; }
public string? AppVersion { get; set; }
public string? OsVersion { get; set; }
public string? DeviceModel { get; set; }
public string? Tags { get; set; }
public bool PushAgreed { get; set; }
public bool MarketingAgreed { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public DateTime? AgreeUpdatedAt { get; set; }
public DateTime? MktAgreeUpdatedAt { get; set; }
// Navigation
public Service Service { get; set; } = null!;
}

View File

@ -0,0 +1,17 @@
namespace SPMS.Domain.Entities;
public class FileEntity : BaseEntity
{
public long ServiceId { get; set; }
public string FileName { get; set; } = string.Empty;
public string FilePath { get; set; } = string.Empty;
public long FileSize { get; set; }
public string FileType { get; set; } = string.Empty;
public string? MimeType { get; set; }
public DateTime CreatedAt { get; set; }
public long CreatedBy { get; set; }
// Navigation
public Service Service { get; set; } = null!;
public Admin CreatedByAdmin { get; set; } = null!;
}

View File

@ -0,0 +1,20 @@
namespace SPMS.Domain.Entities;
public class Message : BaseEntity
{
public long ServiceId { get; set; }
public string MessageCode { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
public string? ImageUrl { get; set; }
public string? LinkUrl { get; set; }
public string? CustomData { get; set; }
public DateTime CreatedAt { get; set; }
public long CreatedBy { get; set; }
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
// Navigation
public Service Service { get; set; } = null!;
public Admin CreatedByAdmin { get; set; } = null!;
}

View File

@ -0,0 +1,20 @@
namespace SPMS.Domain.Entities;
public class Payment : BaseEntity
{
public long ServiceId { get; set; }
public long AdminId { get; set; }
public int Amount { get; set; }
public string Currency { get; set; } = string.Empty;
public string? PaymentMethod { get; set; }
public string? PaymentKey { get; set; }
public byte Status { get; set; }
public byte? TierBefore { get; set; }
public byte TierAfter { get; set; }
public DateTime PaidAt { get; set; }
public DateTime CreatedAt { get; set; }
// Navigation
public Service Service { get; set; } = null!;
public Admin Admin { get; set; } = null!;
}

View File

@ -0,0 +1,14 @@
namespace SPMS.Domain.Entities;
public class PushOpenLog : BaseEntity
{
public long ServiceId { get; set; }
public long MessageId { get; set; }
public long DeviceId { get; set; }
public DateTime OpenedAt { get; set; }
// Navigation
public Service Service { get; set; } = null!;
public Message Message { get; set; } = null!;
public Device Device { get; set; } = null!;
}

View File

@ -0,0 +1,16 @@
namespace SPMS.Domain.Entities;
public class PushSendLog : BaseEntity
{
public long ServiceId { get; set; }
public long MessageId { get; set; }
public long DeviceId { get; set; }
public byte Status { get; set; }
public string? FailReason { get; set; }
public DateTime SentAt { get; set; }
// Navigation
public Service Service { get; set; } = null!;
public Message Message { get; set; } = null!;
public Device Device { get; set; } = null!;
}

View File

@ -0,0 +1,31 @@
namespace SPMS.Domain.Entities;
public class Service : BaseEntity
{
public string ServiceCode { get; set; } = string.Empty;
public string ServiceName { get; set; } = string.Empty;
public string? Description { get; set; }
public string ApiKey { get; set; } = string.Empty;
public DateTime ApiKeyCreatedAt { get; set; }
public string? ApnsBundleId { get; set; }
public string? ApnsKeyId { get; set; }
public string? ApnsTeamId { get; set; }
public string? ApnsPrivateKey { get; set; }
public string? FcmCredentials { get; set; }
public string? WebhookUrl { get; set; }
public string? Tags { get; set; }
public byte SubTier { get; set; }
public DateTime? SubStartedAt { get; set; }
public byte Status { get; set; }
public DateTime CreatedAt { get; set; }
public long CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
// Navigation
public Admin CreatedByAdmin { get; set; } = null!;
public ICollection<ServiceIp> ServiceIps { get; set; } = new List<ServiceIp>();
public ICollection<Device> Devices { get; set; } = new List<Device>();
public ICollection<Message> Messages { get; set; } = new List<Message>();
}

View File

@ -0,0 +1,10 @@
namespace SPMS.Domain.Entities;
public class ServiceIp : BaseEntity
{
public long ServiceId { get; set; }
public string IpAddress { get; set; } = string.Empty;
// Navigation
public Service Service { get; set; } = null!;
}

View File

@ -0,0 +1,17 @@
namespace SPMS.Domain.Entities;
public class SystemLog : BaseEntity
{
public long? ServiceId { get; set; }
public long? AdminId { get; set; }
public string Action { get; set; } = string.Empty;
public string? TargetType { get; set; }
public long? TargetId { get; set; }
public string? Details { get; set; }
public string? IpAddress { get; set; }
public DateTime CreatedAt { get; set; }
// Navigation
public Service? Service { get; set; }
public Admin? Admin { get; set; }
}

View File

@ -0,0 +1,16 @@
namespace SPMS.Domain.Entities;
public class WebhookLog : BaseEntity
{
public long ServiceId { get; set; }
public string WebhookUrl { get; set; } = string.Empty;
public string EventType { get; set; } = string.Empty;
public string Payload { get; set; } = string.Empty;
public byte Status { get; set; }
public int? ResponseCode { get; set; }
public string? ResponseBody { get; set; }
public DateTime SentAt { get; set; }
// Navigation
public Service Service { get; set; } = null!;
}

View File

@ -1,13 +1,30 @@
using Microsoft.EntityFrameworkCore; // 👈 이거 없으면 DbContext에서 빨간 줄 뜹니다! using Microsoft.EntityFrameworkCore;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure; namespace SPMS.Infrastructure;
public class AppDbContext : DbContext public class AppDbContext : DbContext
{ {
// 생성자: API 프로젝트에서 옵션(DB 연결 문자열 등)을 주입받기 위함
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{ {
} }
// 나중에 여기에 DbSet<User> Users { get; set; } 같은 테이블들이 추가될 겁니다. public DbSet<Service> Services => Set<Service>();
} public DbSet<ServiceIp> ServiceIps => Set<ServiceIp>();
public DbSet<Admin> Admins => Set<Admin>();
public DbSet<Device> Devices => Set<Device>();
public DbSet<Message> Messages => Set<Message>();
public DbSet<FileEntity> Files => Set<FileEntity>();
public DbSet<PushSendLog> PushSendLogs => Set<PushSendLog>();
public DbSet<PushOpenLog> PushOpenLogs => Set<PushOpenLog>();
public DbSet<DailyStat> DailyStats => Set<DailyStat>();
public DbSet<WebhookLog> WebhookLogs => Set<WebhookLog>();
public DbSet<SystemLog> SystemLogs => Set<SystemLog>();
public DbSet<Payment> Payments => Set<Payment>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}

View File

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
namespace SPMS.Infrastructure;
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
{
public AppDbContext CreateDbContext(string[] args)
{
var basePath = Path.Combine(Directory.GetCurrentDirectory(), "..", "SPMS.API");
var configuration = new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile("appsettings.Development.json", optional: true)
.AddUserSecrets("1b3e01bb-60ac-40bc-9abf-5cf3a7e35ba2", reloadOnChange: false)
.Build();
var connectionString = configuration.GetConnectionString("DefaultConnection");
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
return new AppDbContext(optionsBuilder.Options);
}
}

View File

@ -0,0 +1,839 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SPMS.Infrastructure;
#nullable disable
namespace SPMS.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260209023245_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("SPMS.Domain.Entities.Admin", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("AdminCode")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("varchar(8)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<bool>("EmailVerified")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(false);
b.Property<DateTime?>("EmailVerifiedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(false);
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("datetime(6)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("varchar(64)");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<sbyte>("Role")
.HasColumnType("tinyint");
b.HasKey("Id");
b.HasIndex("AdminCode")
.IsUnique();
b.HasIndex("Email")
.IsUnique();
b.ToTable("Admin", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.DailyStat", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("FailCnt")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<int>("OpenCnt")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<int>("SentCnt")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.Property<DateOnly>("StatDate")
.HasColumnType("date");
b.Property<int>("SuccessCnt")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.HasKey("Id");
b.HasIndex("ServiceId", "StatDate")
.IsUnique();
b.ToTable("DailyStat", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.Device", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime?>("AgreeUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("AppVersion")
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("DeviceModel")
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<string>("DeviceToken")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(true);
b.Property<bool>("MarketingAgreed")
.HasColumnType("tinyint(1)");
b.Property<DateTime?>("MktAgreeUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("OsVersion")
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<sbyte>("Platform")
.HasColumnType("tinyint");
b.Property<bool>("PushAgreed")
.HasColumnType("tinyint(1)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.Property<string>("Tags")
.HasColumnType("json");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("ServiceId", "DeviceToken");
b.ToTable("Device", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.FileEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("CreatedBy")
.HasColumnType("bigint");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("FilePath")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<long>("FileSize")
.HasColumnType("bigint");
b.Property<string>("FileType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<string>("MimeType")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("CreatedBy");
b.HasIndex("ServiceId");
b.ToTable("File", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.Message", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("CreatedBy")
.HasColumnType("bigint");
b.Property<string>("CustomData")
.HasColumnType("json");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime(6)");
b.Property<string>("ImageUrl")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(false);
b.Property<string>("LinkUrl")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<string>("MessageCode")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("varchar(10)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.HasKey("Id");
b.HasIndex("CreatedBy");
b.HasIndex("MessageCode")
.IsUnique();
b.HasIndex("ServiceId");
b.ToTable("Message", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.Payment", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<long>("AdminId")
.HasColumnType("bigint");
b.Property<int>("Amount")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Currency")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("varchar(10)");
b.Property<DateTime>("PaidAt")
.HasColumnType("datetime(6)");
b.Property<string>("PaymentKey")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("PaymentMethod")
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.Property<sbyte>("Status")
.HasColumnType("tinyint");
b.Property<sbyte>("TierAfter")
.HasColumnType("tinyint");
b.Property<sbyte?>("TierBefore")
.HasColumnType("tinyint");
b.HasKey("Id");
b.HasIndex("AdminId");
b.HasIndex("ServiceId");
b.ToTable("Payment", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.PushOpenLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<long>("DeviceId")
.HasColumnType("bigint");
b.Property<long>("MessageId")
.HasColumnType("bigint");
b.Property<DateTime>("OpenedAt")
.HasColumnType("datetime(6)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("DeviceId");
b.HasIndex("MessageId");
b.HasIndex("ServiceId", "OpenedAt");
b.ToTable("PushOpenLog", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.PushSendLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<long>("DeviceId")
.HasColumnType("bigint");
b.Property<string>("FailReason")
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<long>("MessageId")
.HasColumnType("bigint");
b.Property<DateTime>("SentAt")
.HasColumnType("datetime(6)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.Property<sbyte>("Status")
.HasColumnType("tinyint");
b.HasKey("Id");
b.HasIndex("DeviceId");
b.HasIndex("MessageId");
b.HasIndex("ServiceId", "SentAt");
b.ToTable("PushSendLog", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.Service", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("ApiKey")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("varchar(64)");
b.Property<DateTime>("ApiKeyCreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("ApnsBundleId")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("ApnsKeyId")
.HasMaxLength(10)
.HasColumnType("varchar(10)");
b.Property<string>("ApnsPrivateKey")
.HasColumnType("text");
b.Property<string>("ApnsTeamId")
.HasMaxLength(10)
.HasColumnType("varchar(10)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("CreatedBy")
.HasColumnType("bigint");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<string>("FcmCredentials")
.HasColumnType("text");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(false);
b.Property<string>("ServiceCode")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("varchar(8)");
b.Property<string>("ServiceName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<sbyte>("Status")
.HasColumnType("tinyint");
b.Property<DateTime?>("SubStartedAt")
.HasColumnType("datetime(6)");
b.Property<sbyte>("SubTier")
.HasColumnType("tinyint");
b.Property<string>("Tags")
.HasColumnType("json");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("WebhookUrl")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.HasKey("Id");
b.HasIndex("CreatedBy");
b.HasIndex("ServiceCode")
.IsUnique();
b.ToTable("Service", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.ServiceIp", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(45)
.HasColumnType("varchar(45)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("ServiceId");
b.ToTable("ServiceIp", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.SystemLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Action")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<long?>("AdminId")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Details")
.HasColumnType("json");
b.Property<string>("IpAddress")
.HasMaxLength(45)
.HasColumnType("varchar(45)");
b.Property<long?>("ServiceId")
.HasColumnType("bigint");
b.Property<long?>("TargetId")
.HasColumnType("bigint");
b.Property<string>("TargetType")
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.HasKey("Id");
b.HasIndex("AdminId");
b.HasIndex("CreatedAt");
b.HasIndex("ServiceId");
b.ToTable("SystemLog", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.WebhookLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("EventType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<string>("Payload")
.IsRequired()
.HasColumnType("json");
b.Property<string>("ResponseBody")
.HasColumnType("text");
b.Property<int?>("ResponseCode")
.HasColumnType("int");
b.Property<DateTime>("SentAt")
.HasColumnType("datetime(6)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.Property<sbyte>("Status")
.HasColumnType("tinyint");
b.Property<string>("WebhookUrl")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.HasKey("Id");
b.HasIndex("ServiceId", "SentAt");
b.ToTable("WebhookLog", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.DailyStat", b =>
{
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.Device", b =>
{
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany("Devices")
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.FileEntity", b =>
{
b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin")
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("CreatedByAdmin");
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.Message", b =>
{
b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin")
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany("Messages")
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("CreatedByAdmin");
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.Payment", b =>
{
b.HasOne("SPMS.Domain.Entities.Admin", "Admin")
.WithMany()
.HasForeignKey("AdminId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Admin");
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.PushOpenLog", b =>
{
b.HasOne("SPMS.Domain.Entities.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SPMS.Domain.Entities.Message", "Message")
.WithMany()
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Device");
b.Navigation("Message");
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.PushSendLog", b =>
{
b.HasOne("SPMS.Domain.Entities.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SPMS.Domain.Entities.Message", "Message")
.WithMany()
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Device");
b.Navigation("Message");
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.Service", b =>
{
b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin")
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("CreatedByAdmin");
});
modelBuilder.Entity("SPMS.Domain.Entities.ServiceIp", b =>
{
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany("ServiceIps")
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.SystemLog", b =>
{
b.HasOne("SPMS.Domain.Entities.Admin", "Admin")
.WithMany()
.HasForeignKey("AdminId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Admin");
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.WebhookLog", b =>
{
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.Service", b =>
{
b.Navigation("Devices");
b.Navigation("Messages");
b.Navigation("ServiceIps");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,609 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SPMS.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Admin",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
AdminCode = table.Column<string>(type: "varchar(8)", maxLength: 8, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Email = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Password = table.Column<string>(type: "varchar(64)", maxLength: 64, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Phone = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Name = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Role = table.Column<sbyte>(type: "tinyint", nullable: false),
EmailVerified = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false),
EmailVerifiedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
LastLoginAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
IsDeleted = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false),
DeletedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Admin", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Service",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServiceCode = table.Column<string>(type: "varchar(8)", maxLength: 8, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ServiceName = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Description = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
ApiKey = table.Column<string>(type: "varchar(64)", maxLength: 64, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ApiKeyCreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
ApnsBundleId = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
ApnsKeyId = table.Column<string>(type: "varchar(10)", maxLength: 10, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
ApnsTeamId = table.Column<string>(type: "varchar(10)", maxLength: 10, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
ApnsPrivateKey = table.Column<string>(type: "text", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
FcmCredentials = table.Column<string>(type: "text", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
WebhookUrl = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Tags = table.Column<string>(type: "json", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
SubTier = table.Column<sbyte>(type: "tinyint", nullable: false),
SubStartedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
Status = table.Column<sbyte>(type: "tinyint", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
CreatedBy = table.Column<long>(type: "bigint", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
IsDeleted = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false),
DeletedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Service", x => x.Id);
table.ForeignKey(
name: "FK_Service_Admin_CreatedBy",
column: x => x.CreatedBy,
principalTable: "Admin",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "DailyStat",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServiceId = table.Column<long>(type: "bigint", nullable: false),
StatDate = table.Column<DateOnly>(type: "date", nullable: false),
SentCnt = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
SuccessCnt = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
FailCnt = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
OpenCnt = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DailyStat", x => x.Id);
table.ForeignKey(
name: "FK_DailyStat_Service_ServiceId",
column: x => x.ServiceId,
principalTable: "Service",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Device",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServiceId = table.Column<long>(type: "bigint", nullable: false),
DeviceToken = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Platform = table.Column<sbyte>(type: "tinyint", nullable: false),
AppVersion = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
OsVersion = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
DeviceModel = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Tags = table.Column<string>(type: "json", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
PushAgreed = table.Column<bool>(type: "tinyint(1)", nullable: false),
MarketingAgreed = table.Column<bool>(type: "tinyint(1)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
AgreeUpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
MktAgreeUpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Device", x => x.Id);
table.ForeignKey(
name: "FK_Device_Service_ServiceId",
column: x => x.ServiceId,
principalTable: "Service",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "File",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServiceId = table.Column<long>(type: "bigint", nullable: false),
FileName = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
FilePath = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
FileSize = table.Column<long>(type: "bigint", nullable: false),
FileType = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
MimeType = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
CreatedBy = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_File", x => x.Id);
table.ForeignKey(
name: "FK_File_Admin_CreatedBy",
column: x => x.CreatedBy,
principalTable: "Admin",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_File_Service_ServiceId",
column: x => x.ServiceId,
principalTable: "Service",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Message",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServiceId = table.Column<long>(type: "bigint", nullable: false),
MessageCode = table.Column<string>(type: "varchar(10)", maxLength: 10, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Title = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Body = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ImageUrl = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
LinkUrl = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
CustomData = table.Column<string>(type: "json", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
CreatedBy = table.Column<long>(type: "bigint", nullable: false),
IsDeleted = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false),
DeletedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Message", x => x.Id);
table.ForeignKey(
name: "FK_Message_Admin_CreatedBy",
column: x => x.CreatedBy,
principalTable: "Admin",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Message_Service_ServiceId",
column: x => x.ServiceId,
principalTable: "Service",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Payment",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServiceId = table.Column<long>(type: "bigint", nullable: false),
AdminId = table.Column<long>(type: "bigint", nullable: false),
Amount = table.Column<int>(type: "int", nullable: false),
Currency = table.Column<string>(type: "varchar(10)", maxLength: 10, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
PaymentMethod = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
PaymentKey = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Status = table.Column<sbyte>(type: "tinyint", nullable: false),
TierBefore = table.Column<sbyte>(type: "tinyint", nullable: true),
TierAfter = table.Column<sbyte>(type: "tinyint", nullable: false),
PaidAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Payment", x => x.Id);
table.ForeignKey(
name: "FK_Payment_Admin_AdminId",
column: x => x.AdminId,
principalTable: "Admin",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Payment_Service_ServiceId",
column: x => x.ServiceId,
principalTable: "Service",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "ServiceIp",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServiceId = table.Column<long>(type: "bigint", nullable: false),
IpAddress = table.Column<string>(type: "varchar(45)", maxLength: 45, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_ServiceIp", x => x.Id);
table.ForeignKey(
name: "FK_ServiceIp_Service_ServiceId",
column: x => x.ServiceId,
principalTable: "Service",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "SystemLog",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServiceId = table.Column<long>(type: "bigint", nullable: true),
AdminId = table.Column<long>(type: "bigint", nullable: true),
Action = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
TargetType = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
TargetId = table.Column<long>(type: "bigint", nullable: true),
Details = table.Column<string>(type: "json", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
IpAddress = table.Column<string>(type: "varchar(45)", maxLength: 45, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SystemLog", x => x.Id);
table.ForeignKey(
name: "FK_SystemLog_Admin_AdminId",
column: x => x.AdminId,
principalTable: "Admin",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_SystemLog_Service_ServiceId",
column: x => x.ServiceId,
principalTable: "Service",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "WebhookLog",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServiceId = table.Column<long>(type: "bigint", nullable: false),
WebhookUrl = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
EventType = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Payload = table.Column<string>(type: "json", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Status = table.Column<sbyte>(type: "tinyint", nullable: false),
ResponseCode = table.Column<int>(type: "int", nullable: true),
ResponseBody = table.Column<string>(type: "text", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
SentAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_WebhookLog", x => x.Id);
table.ForeignKey(
name: "FK_WebhookLog_Service_ServiceId",
column: x => x.ServiceId,
principalTable: "Service",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "PushOpenLog",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServiceId = table.Column<long>(type: "bigint", nullable: false),
MessageId = table.Column<long>(type: "bigint", nullable: false),
DeviceId = table.Column<long>(type: "bigint", nullable: false),
OpenedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PushOpenLog", x => x.Id);
table.ForeignKey(
name: "FK_PushOpenLog_Device_DeviceId",
column: x => x.DeviceId,
principalTable: "Device",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_PushOpenLog_Message_MessageId",
column: x => x.MessageId,
principalTable: "Message",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_PushOpenLog_Service_ServiceId",
column: x => x.ServiceId,
principalTable: "Service",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "PushSendLog",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ServiceId = table.Column<long>(type: "bigint", nullable: false),
MessageId = table.Column<long>(type: "bigint", nullable: false),
DeviceId = table.Column<long>(type: "bigint", nullable: false),
Status = table.Column<sbyte>(type: "tinyint", nullable: false),
FailReason = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
SentAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PushSendLog", x => x.Id);
table.ForeignKey(
name: "FK_PushSendLog_Device_DeviceId",
column: x => x.DeviceId,
principalTable: "Device",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_PushSendLog_Message_MessageId",
column: x => x.MessageId,
principalTable: "Message",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_PushSendLog_Service_ServiceId",
column: x => x.ServiceId,
principalTable: "Service",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_Admin_AdminCode",
table: "Admin",
column: "AdminCode",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Admin_Email",
table: "Admin",
column: "Email",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_DailyStat_ServiceId_StatDate",
table: "DailyStat",
columns: new[] { "ServiceId", "StatDate" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Device_ServiceId_DeviceToken",
table: "Device",
columns: new[] { "ServiceId", "DeviceToken" });
migrationBuilder.CreateIndex(
name: "IX_File_CreatedBy",
table: "File",
column: "CreatedBy");
migrationBuilder.CreateIndex(
name: "IX_File_ServiceId",
table: "File",
column: "ServiceId");
migrationBuilder.CreateIndex(
name: "IX_Message_CreatedBy",
table: "Message",
column: "CreatedBy");
migrationBuilder.CreateIndex(
name: "IX_Message_MessageCode",
table: "Message",
column: "MessageCode",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Message_ServiceId",
table: "Message",
column: "ServiceId");
migrationBuilder.CreateIndex(
name: "IX_Payment_AdminId",
table: "Payment",
column: "AdminId");
migrationBuilder.CreateIndex(
name: "IX_Payment_ServiceId",
table: "Payment",
column: "ServiceId");
migrationBuilder.CreateIndex(
name: "IX_PushOpenLog_DeviceId",
table: "PushOpenLog",
column: "DeviceId");
migrationBuilder.CreateIndex(
name: "IX_PushOpenLog_MessageId",
table: "PushOpenLog",
column: "MessageId");
migrationBuilder.CreateIndex(
name: "IX_PushOpenLog_ServiceId_OpenedAt",
table: "PushOpenLog",
columns: new[] { "ServiceId", "OpenedAt" });
migrationBuilder.CreateIndex(
name: "IX_PushSendLog_DeviceId",
table: "PushSendLog",
column: "DeviceId");
migrationBuilder.CreateIndex(
name: "IX_PushSendLog_MessageId",
table: "PushSendLog",
column: "MessageId");
migrationBuilder.CreateIndex(
name: "IX_PushSendLog_ServiceId_SentAt",
table: "PushSendLog",
columns: new[] { "ServiceId", "SentAt" });
migrationBuilder.CreateIndex(
name: "IX_Service_CreatedBy",
table: "Service",
column: "CreatedBy");
migrationBuilder.CreateIndex(
name: "IX_Service_ServiceCode",
table: "Service",
column: "ServiceCode",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ServiceIp_ServiceId",
table: "ServiceIp",
column: "ServiceId");
migrationBuilder.CreateIndex(
name: "IX_SystemLog_AdminId",
table: "SystemLog",
column: "AdminId");
migrationBuilder.CreateIndex(
name: "IX_SystemLog_CreatedAt",
table: "SystemLog",
column: "CreatedAt");
migrationBuilder.CreateIndex(
name: "IX_SystemLog_ServiceId",
table: "SystemLog",
column: "ServiceId");
migrationBuilder.CreateIndex(
name: "IX_WebhookLog_ServiceId_SentAt",
table: "WebhookLog",
columns: new[] { "ServiceId", "SentAt" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DailyStat");
migrationBuilder.DropTable(
name: "File");
migrationBuilder.DropTable(
name: "Payment");
migrationBuilder.DropTable(
name: "PushOpenLog");
migrationBuilder.DropTable(
name: "PushSendLog");
migrationBuilder.DropTable(
name: "ServiceIp");
migrationBuilder.DropTable(
name: "SystemLog");
migrationBuilder.DropTable(
name: "WebhookLog");
migrationBuilder.DropTable(
name: "Device");
migrationBuilder.DropTable(
name: "Message");
migrationBuilder.DropTable(
name: "Service");
migrationBuilder.DropTable(
name: "Admin");
}
}
}

View File

@ -0,0 +1,836 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SPMS.Infrastructure;
#nullable disable
namespace SPMS.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("SPMS.Domain.Entities.Admin", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("AdminCode")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("varchar(8)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<bool>("EmailVerified")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(false);
b.Property<DateTime?>("EmailVerifiedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(false);
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("datetime(6)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("varchar(64)");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<sbyte>("Role")
.HasColumnType("tinyint");
b.HasKey("Id");
b.HasIndex("AdminCode")
.IsUnique();
b.HasIndex("Email")
.IsUnique();
b.ToTable("Admin", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.DailyStat", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("FailCnt")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<int>("OpenCnt")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<int>("SentCnt")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.Property<DateOnly>("StatDate")
.HasColumnType("date");
b.Property<int>("SuccessCnt")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.HasKey("Id");
b.HasIndex("ServiceId", "StatDate")
.IsUnique();
b.ToTable("DailyStat", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.Device", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime?>("AgreeUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("AppVersion")
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("DeviceModel")
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<string>("DeviceToken")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(true);
b.Property<bool>("MarketingAgreed")
.HasColumnType("tinyint(1)");
b.Property<DateTime?>("MktAgreeUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("OsVersion")
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<sbyte>("Platform")
.HasColumnType("tinyint");
b.Property<bool>("PushAgreed")
.HasColumnType("tinyint(1)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.Property<string>("Tags")
.HasColumnType("json");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("ServiceId", "DeviceToken");
b.ToTable("Device", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.FileEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("CreatedBy")
.HasColumnType("bigint");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("FilePath")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<long>("FileSize")
.HasColumnType("bigint");
b.Property<string>("FileType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<string>("MimeType")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("CreatedBy");
b.HasIndex("ServiceId");
b.ToTable("File", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.Message", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("CreatedBy")
.HasColumnType("bigint");
b.Property<string>("CustomData")
.HasColumnType("json");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime(6)");
b.Property<string>("ImageUrl")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(false);
b.Property<string>("LinkUrl")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<string>("MessageCode")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("varchar(10)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.HasKey("Id");
b.HasIndex("CreatedBy");
b.HasIndex("MessageCode")
.IsUnique();
b.HasIndex("ServiceId");
b.ToTable("Message", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.Payment", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<long>("AdminId")
.HasColumnType("bigint");
b.Property<int>("Amount")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Currency")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("varchar(10)");
b.Property<DateTime>("PaidAt")
.HasColumnType("datetime(6)");
b.Property<string>("PaymentKey")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("PaymentMethod")
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.Property<sbyte>("Status")
.HasColumnType("tinyint");
b.Property<sbyte>("TierAfter")
.HasColumnType("tinyint");
b.Property<sbyte?>("TierBefore")
.HasColumnType("tinyint");
b.HasKey("Id");
b.HasIndex("AdminId");
b.HasIndex("ServiceId");
b.ToTable("Payment", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.PushOpenLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<long>("DeviceId")
.HasColumnType("bigint");
b.Property<long>("MessageId")
.HasColumnType("bigint");
b.Property<DateTime>("OpenedAt")
.HasColumnType("datetime(6)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("DeviceId");
b.HasIndex("MessageId");
b.HasIndex("ServiceId", "OpenedAt");
b.ToTable("PushOpenLog", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.PushSendLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<long>("DeviceId")
.HasColumnType("bigint");
b.Property<string>("FailReason")
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<long>("MessageId")
.HasColumnType("bigint");
b.Property<DateTime>("SentAt")
.HasColumnType("datetime(6)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.Property<sbyte>("Status")
.HasColumnType("tinyint");
b.HasKey("Id");
b.HasIndex("DeviceId");
b.HasIndex("MessageId");
b.HasIndex("ServiceId", "SentAt");
b.ToTable("PushSendLog", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.Service", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("ApiKey")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("varchar(64)");
b.Property<DateTime>("ApiKeyCreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("ApnsBundleId")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("ApnsKeyId")
.HasMaxLength(10)
.HasColumnType("varchar(10)");
b.Property<string>("ApnsPrivateKey")
.HasColumnType("text");
b.Property<string>("ApnsTeamId")
.HasMaxLength(10)
.HasColumnType("varchar(10)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("CreatedBy")
.HasColumnType("bigint");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<string>("FcmCredentials")
.HasColumnType("text");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(false);
b.Property<string>("ServiceCode")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("varchar(8)");
b.Property<string>("ServiceName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<sbyte>("Status")
.HasColumnType("tinyint");
b.Property<DateTime?>("SubStartedAt")
.HasColumnType("datetime(6)");
b.Property<sbyte>("SubTier")
.HasColumnType("tinyint");
b.Property<string>("Tags")
.HasColumnType("json");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("WebhookUrl")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.HasKey("Id");
b.HasIndex("CreatedBy");
b.HasIndex("ServiceCode")
.IsUnique();
b.ToTable("Service", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.ServiceIp", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(45)
.HasColumnType("varchar(45)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("ServiceId");
b.ToTable("ServiceIp", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.SystemLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Action")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<long?>("AdminId")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Details")
.HasColumnType("json");
b.Property<string>("IpAddress")
.HasMaxLength(45)
.HasColumnType("varchar(45)");
b.Property<long?>("ServiceId")
.HasColumnType("bigint");
b.Property<long?>("TargetId")
.HasColumnType("bigint");
b.Property<string>("TargetType")
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.HasKey("Id");
b.HasIndex("AdminId");
b.HasIndex("CreatedAt");
b.HasIndex("ServiceId");
b.ToTable("SystemLog", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.WebhookLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("EventType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<string>("Payload")
.IsRequired()
.HasColumnType("json");
b.Property<string>("ResponseBody")
.HasColumnType("text");
b.Property<int?>("ResponseCode")
.HasColumnType("int");
b.Property<DateTime>("SentAt")
.HasColumnType("datetime(6)");
b.Property<long>("ServiceId")
.HasColumnType("bigint");
b.Property<sbyte>("Status")
.HasColumnType("tinyint");
b.Property<string>("WebhookUrl")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.HasKey("Id");
b.HasIndex("ServiceId", "SentAt");
b.ToTable("WebhookLog", (string)null);
});
modelBuilder.Entity("SPMS.Domain.Entities.DailyStat", b =>
{
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.Device", b =>
{
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany("Devices")
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.FileEntity", b =>
{
b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin")
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("CreatedByAdmin");
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.Message", b =>
{
b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin")
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany("Messages")
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("CreatedByAdmin");
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.Payment", b =>
{
b.HasOne("SPMS.Domain.Entities.Admin", "Admin")
.WithMany()
.HasForeignKey("AdminId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Admin");
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.PushOpenLog", b =>
{
b.HasOne("SPMS.Domain.Entities.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SPMS.Domain.Entities.Message", "Message")
.WithMany()
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Device");
b.Navigation("Message");
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.PushSendLog", b =>
{
b.HasOne("SPMS.Domain.Entities.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SPMS.Domain.Entities.Message", "Message")
.WithMany()
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Device");
b.Navigation("Message");
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.Service", b =>
{
b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin")
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("CreatedByAdmin");
});
modelBuilder.Entity("SPMS.Domain.Entities.ServiceIp", b =>
{
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany("ServiceIps")
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.SystemLog", b =>
{
b.HasOne("SPMS.Domain.Entities.Admin", "Admin")
.WithMany()
.HasForeignKey("AdminId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Admin");
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.WebhookLog", b =>
{
b.HasOne("SPMS.Domain.Entities.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Service");
});
modelBuilder.Entity("SPMS.Domain.Entities.Service", b =>
{
b.Navigation("Devices");
b.Navigation("Messages");
b.Navigation("ServiceIps");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure.Persistence.Configurations;
public class AdminConfiguration : IEntityTypeConfiguration<Admin>
{
public void Configure(EntityTypeBuilder<Admin> builder)
{
builder.ToTable("Admin");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.AdminCode).HasMaxLength(8).IsRequired();
builder.HasIndex(e => e.AdminCode).IsUnique();
builder.Property(e => e.Email).HasMaxLength(100).IsRequired();
builder.HasIndex(e => e.Email).IsUnique();
builder.Property(e => e.Password).HasMaxLength(64).IsRequired();
builder.Property(e => e.Phone).HasMaxLength(20).IsRequired();
builder.Property(e => e.Name).HasMaxLength(50).IsRequired();
builder.Property(e => e.Role).HasColumnType("tinyint").IsRequired();
builder.Property(e => e.EmailVerified).HasColumnType("tinyint(1)").IsRequired().HasDefaultValue(false);
builder.Property(e => e.EmailVerifiedAt);
builder.Property(e => e.CreatedAt).IsRequired();
builder.Property(e => e.LastLoginAt);
builder.Property(e => e.IsDeleted).HasColumnType("tinyint(1)").IsRequired().HasDefaultValue(false);
builder.Property(e => e.DeletedAt);
builder.HasQueryFilter(e => !e.IsDeleted);
}
}

View File

@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure.Persistence.Configurations;
public class DailyStatConfiguration : IEntityTypeConfiguration<DailyStat>
{
public void Configure(EntityTypeBuilder<DailyStat> builder)
{
builder.ToTable("DailyStat");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.ServiceId).IsRequired();
builder.Property(e => e.StatDate).IsRequired();
builder.Property(e => e.SentCnt).IsRequired().HasDefaultValue(0);
builder.Property(e => e.SuccessCnt).IsRequired().HasDefaultValue(0);
builder.Property(e => e.FailCnt).IsRequired().HasDefaultValue(0);
builder.Property(e => e.OpenCnt).IsRequired().HasDefaultValue(0);
builder.Property(e => e.CreatedAt).IsRequired();
builder.HasOne(e => e.Service)
.WithMany()
.HasForeignKey(e => e.ServiceId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(e => new { e.ServiceId, e.StatDate }).IsUnique();
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure.Persistence.Configurations;
public class DeviceConfiguration : IEntityTypeConfiguration<Device>
{
public void Configure(EntityTypeBuilder<Device> builder)
{
builder.ToTable("Device");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.ServiceId).IsRequired();
builder.Property(e => e.DeviceToken).HasMaxLength(255).IsRequired();
builder.Property(e => e.Platform).HasColumnType("tinyint").IsRequired();
builder.Property(e => e.AppVersion).HasMaxLength(20);
builder.Property(e => e.OsVersion).HasMaxLength(20);
builder.Property(e => e.DeviceModel).HasMaxLength(50);
builder.Property(e => e.Tags).HasColumnType("json");
builder.Property(e => e.PushAgreed).HasColumnType("tinyint(1)").IsRequired();
builder.Property(e => e.MarketingAgreed).HasColumnType("tinyint(1)").IsRequired();
builder.Property(e => e.IsActive).HasColumnType("tinyint(1)").IsRequired().HasDefaultValue(true);
builder.Property(e => e.CreatedAt).IsRequired();
builder.Property(e => e.UpdatedAt);
builder.Property(e => e.AgreeUpdatedAt);
builder.Property(e => e.MktAgreeUpdatedAt);
builder.HasOne(e => e.Service)
.WithMany(s => s.Devices)
.HasForeignKey(e => e.ServiceId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(e => new { e.ServiceId, e.DeviceToken });
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure.Persistence.Configurations;
public class FileEntityConfiguration : IEntityTypeConfiguration<FileEntity>
{
public void Configure(EntityTypeBuilder<FileEntity> builder)
{
builder.ToTable("File");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.ServiceId).IsRequired();
builder.Property(e => e.FileName).HasMaxLength(200).IsRequired();
builder.Property(e => e.FilePath).HasMaxLength(500).IsRequired();
builder.Property(e => e.FileSize).IsRequired();
builder.Property(e => e.FileType).HasMaxLength(20).IsRequired();
builder.Property(e => e.MimeType).HasMaxLength(100);
builder.Property(e => e.CreatedAt).IsRequired();
builder.Property(e => e.CreatedBy).IsRequired();
builder.HasOne(e => e.Service)
.WithMany()
.HasForeignKey(e => e.ServiceId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.CreatedByAdmin)
.WithMany()
.HasForeignKey(e => e.CreatedBy)
.OnDelete(DeleteBehavior.Restrict);
}
}

View File

@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure.Persistence.Configurations;
public class MessageConfiguration : IEntityTypeConfiguration<Message>
{
public void Configure(EntityTypeBuilder<Message> builder)
{
builder.ToTable("Message");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.ServiceId).IsRequired();
builder.Property(e => e.MessageCode).HasMaxLength(10).IsRequired();
builder.HasIndex(e => e.MessageCode).IsUnique();
builder.Property(e => e.Title).HasMaxLength(100).IsRequired();
builder.Property(e => e.Body).HasMaxLength(500).IsRequired();
builder.Property(e => e.ImageUrl).HasMaxLength(500);
builder.Property(e => e.LinkUrl).HasMaxLength(500);
builder.Property(e => e.CustomData).HasColumnType("json");
builder.Property(e => e.CreatedAt).IsRequired();
builder.Property(e => e.CreatedBy).IsRequired();
builder.Property(e => e.IsDeleted).HasColumnType("tinyint(1)").IsRequired().HasDefaultValue(false);
builder.Property(e => e.DeletedAt);
builder.HasOne(e => e.Service)
.WithMany(s => s.Messages)
.HasForeignKey(e => e.ServiceId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.CreatedByAdmin)
.WithMany()
.HasForeignKey(e => e.CreatedBy)
.OnDelete(DeleteBehavior.Restrict);
builder.HasQueryFilter(e => !e.IsDeleted);
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure.Persistence.Configurations;
public class PaymentConfiguration : IEntityTypeConfiguration<Payment>
{
public void Configure(EntityTypeBuilder<Payment> builder)
{
builder.ToTable("Payment");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.ServiceId).IsRequired();
builder.Property(e => e.AdminId).IsRequired();
builder.Property(e => e.Amount).IsRequired();
builder.Property(e => e.Currency).HasMaxLength(10).IsRequired();
builder.Property(e => e.PaymentMethod).HasMaxLength(50);
builder.Property(e => e.PaymentKey).HasMaxLength(100);
builder.Property(e => e.Status).HasColumnType("tinyint").IsRequired();
builder.Property(e => e.TierBefore).HasColumnType("tinyint");
builder.Property(e => e.TierAfter).HasColumnType("tinyint").IsRequired();
builder.Property(e => e.PaidAt).IsRequired();
builder.Property(e => e.CreatedAt).IsRequired();
builder.HasOne(e => e.Service)
.WithMany()
.HasForeignKey(e => e.ServiceId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.Admin)
.WithMany()
.HasForeignKey(e => e.AdminId)
.OnDelete(DeleteBehavior.Restrict);
}
}

View File

@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure.Persistence.Configurations;
public class PushOpenLogConfiguration : IEntityTypeConfiguration<PushOpenLog>
{
public void Configure(EntityTypeBuilder<PushOpenLog> builder)
{
builder.ToTable("PushOpenLog");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.ServiceId).IsRequired();
builder.Property(e => e.MessageId).IsRequired();
builder.Property(e => e.DeviceId).IsRequired();
builder.Property(e => e.OpenedAt).IsRequired();
builder.HasOne(e => e.Service)
.WithMany()
.HasForeignKey(e => e.ServiceId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.Message)
.WithMany()
.HasForeignKey(e => e.MessageId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.Device)
.WithMany()
.HasForeignKey(e => e.DeviceId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(e => new { e.ServiceId, e.OpenedAt });
builder.HasIndex(e => e.MessageId);
}
}

View File

@ -0,0 +1,41 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure.Persistence.Configurations;
public class PushSendLogConfiguration : IEntityTypeConfiguration<PushSendLog>
{
public void Configure(EntityTypeBuilder<PushSendLog> builder)
{
builder.ToTable("PushSendLog");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.ServiceId).IsRequired();
builder.Property(e => e.MessageId).IsRequired();
builder.Property(e => e.DeviceId).IsRequired();
builder.Property(e => e.Status).HasColumnType("tinyint").IsRequired();
builder.Property(e => e.FailReason).HasMaxLength(200);
builder.Property(e => e.SentAt).IsRequired();
builder.HasOne(e => e.Service)
.WithMany()
.HasForeignKey(e => e.ServiceId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.Message)
.WithMany()
.HasForeignKey(e => e.MessageId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.Device)
.WithMany()
.HasForeignKey(e => e.DeviceId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(e => new { e.ServiceId, e.SentAt });
builder.HasIndex(e => e.MessageId);
}
}

View File

@ -0,0 +1,46 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure.Persistence.Configurations;
public class ServiceConfiguration : IEntityTypeConfiguration<Service>
{
public void Configure(EntityTypeBuilder<Service> builder)
{
builder.ToTable("Service");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.ServiceCode).HasMaxLength(8).IsRequired();
builder.HasIndex(e => e.ServiceCode).IsUnique();
builder.Property(e => e.ServiceName).HasMaxLength(100).IsRequired();
builder.Property(e => e.Description).HasMaxLength(500);
builder.Property(e => e.ApiKey).HasMaxLength(64).IsRequired();
builder.Property(e => e.ApiKeyCreatedAt).IsRequired();
builder.Property(e => e.ApnsBundleId).HasMaxLength(100);
builder.Property(e => e.ApnsKeyId).HasMaxLength(10);
builder.Property(e => e.ApnsTeamId).HasMaxLength(10);
builder.Property(e => e.ApnsPrivateKey).HasColumnType("text");
builder.Property(e => e.FcmCredentials).HasColumnType("text");
builder.Property(e => e.WebhookUrl).HasMaxLength(500);
builder.Property(e => e.Tags).HasColumnType("json");
builder.Property(e => e.SubTier).HasColumnType("tinyint").IsRequired();
builder.Property(e => e.SubStartedAt);
builder.Property(e => e.Status).HasColumnType("tinyint").IsRequired();
builder.Property(e => e.CreatedAt).IsRequired();
builder.Property(e => e.CreatedBy).IsRequired();
builder.Property(e => e.UpdatedAt);
builder.Property(e => e.IsDeleted).HasColumnType("tinyint(1)").IsRequired().HasDefaultValue(false);
builder.Property(e => e.DeletedAt);
builder.HasOne(e => e.CreatedByAdmin)
.WithMany()
.HasForeignKey(e => e.CreatedBy)
.OnDelete(DeleteBehavior.Restrict);
builder.HasQueryFilter(e => !e.IsDeleted);
}
}

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure.Persistence.Configurations;
public class ServiceIpConfiguration : IEntityTypeConfiguration<ServiceIp>
{
public void Configure(EntityTypeBuilder<ServiceIp> builder)
{
builder.ToTable("ServiceIp");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.ServiceId).IsRequired();
builder.Property(e => e.IpAddress).HasMaxLength(45).IsRequired();
builder.HasOne(e => e.Service)
.WithMany(s => s.ServiceIps)
.HasForeignKey(e => e.ServiceId)
.OnDelete(DeleteBehavior.Cascade);
}
}

View File

@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure.Persistence.Configurations;
public class SystemLogConfiguration : IEntityTypeConfiguration<SystemLog>
{
public void Configure(EntityTypeBuilder<SystemLog> builder)
{
builder.ToTable("SystemLog");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.ServiceId);
builder.Property(e => e.AdminId);
builder.Property(e => e.Action).HasMaxLength(100).IsRequired();
builder.Property(e => e.TargetType).HasMaxLength(50);
builder.Property(e => e.TargetId);
builder.Property(e => e.Details).HasColumnType("json");
builder.Property(e => e.IpAddress).HasMaxLength(45);
builder.Property(e => e.CreatedAt).IsRequired();
builder.HasOne(e => e.Service)
.WithMany()
.HasForeignKey(e => e.ServiceId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.Admin)
.WithMany()
.HasForeignKey(e => e.AdminId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(e => e.CreatedAt);
}
}

View File

@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SPMS.Domain.Entities;
namespace SPMS.Infrastructure.Persistence.Configurations;
public class WebhookLogConfiguration : IEntityTypeConfiguration<WebhookLog>
{
public void Configure(EntityTypeBuilder<WebhookLog> builder)
{
builder.ToTable("WebhookLog");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.ServiceId).IsRequired();
builder.Property(e => e.WebhookUrl).HasMaxLength(500).IsRequired();
builder.Property(e => e.EventType).HasMaxLength(50).IsRequired();
builder.Property(e => e.Payload).HasColumnType("json").IsRequired();
builder.Property(e => e.Status).HasColumnType("tinyint").IsRequired();
builder.Property(e => e.ResponseCode);
builder.Property(e => e.ResponseBody).HasColumnType("text");
builder.Property(e => e.SentAt).IsRequired();
builder.HasOne(e => e.Service)
.WithMany()
.HasForeignKey(e => e.ServiceId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(e => new { e.ServiceId, e.SentAt });
}
}

View File

@ -13,6 +13,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>