# Thực tập doanh nghiệp (/thuc-tap-doanh-nghiep)
Cổng thực tập giúp nhà trường tổ chức các đợt thực tập: doanh nghiệp đăng ký tham gia, nhà trường duyệt, sinh viên tìm doanh nghiệp và nộp hồ sơ, doanh nghiệp nhận hoặc từ chối. Mỗi trường có cổng riêng, với tên, màu sắc và quy định riêng.
## Cách một đợt thực tập diễn ra [#cách-một-đợt-thực-tập-diễn-ra]
**Nhà trường mở đợt** và đưa danh sách sinh viên vào đợt.
**Doanh nghiệp gửi hồ sơ tham gia đợt**, nhà trường xem và duyệt.
**Sinh viên tìm doanh nghiệp** trên trang chủ của trường và nộp hồ sơ kèm CV.
**Doanh nghiệp nhận hoặc từ chối.** Sinh viên được nhận là có nơi thực tập ngay.
**Nhà trường theo dõi** tình hình và đóng đợt khi kết thúc.
## Các từ dùng trong tài liệu [#các-từ-dùng-trong-tài-liệu]
| Từ | Nghĩa |
| --- | --- |
| Đợt thực tập | Khoảng thời gian nhà trường mở để doanh nghiệp tham gia và sinh viên nộp hồ sơ. Mỗi lúc chỉ có một đợt mở. |
| Hồ sơ tham gia | Thông tin doanh nghiệp gửi để nhà trường duyệt cho một đợt. |
| Hồ sơ ứng tuyển | Hồ sơ sinh viên nộp vào một doanh nghiệp, kèm CV. |
| Chỉ tiêu | Số hồ sơ chờ tối đa và số sinh viên tối đa một doanh nghiệp được nhận trong đợt, do nhà trường đặt. |
| Có nơi thực tập | Sinh viên đã được một doanh nghiệp nhận. |
| Đã đóng | Hồ sơ ứng tuyển không còn được xét, vì sinh viên đã có nơi khác hoặc bị nhà trường khoá. |
## Hướng dẫn theo vai trò [#hướng-dẫn-theo-vai-trò]
## Dành cho đơn vị phụ trách hệ thống [#dành-cho-đơn-vị-phụ-trách-hệ-thống]
Phần mềm quản lý đào tạo của trường đọc được danh sách và kết quả thực tập qua API theo chuẩn OneRoster 1.2. Xem [Tổng quan API](./lap-trinh/oneroster/index.mdx).
# Đọc dữ liệu (/thuc-tap-doanh-nghiep/lap-trinh/api-cong/doc-du-lieu)
Mọi đường dẫn dưới đây tính từ `https://
./integration/v1`.
| Lệnh | Đường dẫn | Quyền |
| --- | --- | --- |
| `GET` | `/seasons` | `seasons:read` |
| `GET` | `/seasons/{id}` | `seasons:read` |
| `GET` | `/students` | `students:read` |
| `GET` | `/students/{studentCode}` | `students:read` |
| `GET` | `/seasons/{id}/enrollments` | `enrollments:read` |
| `GET` | `/seasons/{id}/results` | `results:read` |
| `GET` | `/seasons/{id}/companies` | `companies:read` |
## Đợt thực tập [#đợt-thực-tập]
`GET /seasons` trả mọi đợt của trường (tối đa 500, cũ trước). Lọc theo mã kỳ học: `?externalId=HK1-2026`.
```json
{
"data": [
{
"id": "k3v9x2m1q8w7e6r5t4y3u2i1",
"externalId": "HK1-2026",
"name": "Đợt thực tập học kỳ 1 năm học 2026-2027",
"status": "open",
"openedAt": "2026-09-01T01:00:00.000Z",
"closesAt": "2027-01-15T16:59:59.000Z",
"closedAt": null,
"updatedAt": "2026-11-02T03:12:44.000Z"
}
]
}
```
`status`: `open` (đang mở), `upcoming` (chưa mở), `closed` (đã đóng). Mỗi trường chỉ có một đợt đang mở.
## Sinh viên [#sinh-viên]
`GET /students` trả sinh viên của trường theo trang (xem [Phân trang](./index.mdx#phân-trang)).
| Tham số | Ý nghĩa |
| --- | --- |
| `limit`, `cursor` | Phân trang |
| `updatedSince` | Chỉ sinh viên có thay đổi từ thời điểm này (ISO 8601) |
| `status` | `active` hoặc `blocked` |
| `season` | `id` của đợt: chỉ sinh viên trong đợt đó |
```json
{
"data": [
{
"id": "p0o9i8u7y6t5r4e3w2q1a2s3",
"studentCode": "SV2026001",
"externalId": "U-1001",
"name": "Nguyễn Văn An",
"email": "an.nv@truong.edu.vn",
"signIn": "google",
"loginName": null,
"status": "active",
"createdAt": "2026-09-02T02:00:00.000Z",
"updatedAt": "2026-11-02T03:12:44.000Z"
}
],
"next": "eyJhZnRlciI6MTIzfQ"
}
```
- `signIn`: `google` (đăng nhập bằng tài khoản Google theo `email`) hoặc `password` (đăng nhập bằng `loginName` và mật khẩu).
- `email` là `null` khi sinh viên đăng nhập bằng tên đăng nhập và chưa có email thật.
`GET /students/{studentCode}` trả một sinh viên trong `data`, hoặc `404`.
## Sinh viên trong đợt [#sinh-viên-trong-đợt]
`GET /seasons/{id}/enrollments` trả theo trang:
```json
{
"data": [
{
"id": "z1x2c3v4b5n6m7a8s9d0f1g2",
"externalId": null,
"season": "k3v9x2m1q8w7e6r5t4y3u2i1",
"student": { "id": "p0o9i8u7y6t5r4e3w2q1a2s3", "studentCode": "SV2026001", "externalId": "U-1001", "name": "Nguyễn Văn An" },
"placed": false,
"admittedAt": "2026-09-02T02:00:00.000Z"
}
],
"next": null
}
```
`placed`: sinh viên đã có nơi thực tập trong đợt này chưa.
## Kết quả thực tập [#kết-quả-thực-tập]
`GET /seasons/{id}/results` trả, cho mỗi sinh viên trong đợt, nơi thực tập đã chốt:
```json
{
"data": [
{
"student": { "id": "p0o9i8u7y6t5r4e3w2q1a2s3", "studentCode": "SV2026001", "externalId": "U-1001", "name": "Nguyễn Văn An" },
"season": "k3v9x2m1q8w7e6r5t4y3u2i1",
"placed": true,
"company": { "id": "c8d7e6f5g4h3j2k1l0m9n8b7", "name": "Công ty Cổ phần Ví dụ" },
"acceptedAt": "2026-10-05T08:30:00.000Z",
"pendingApplications": 0
}
],
"next": null
}
```
- `placed: false` và `company: null`: chưa có nơi thực tập.
- `pendingApplications`: số hồ sơ đang chờ doanh nghiệp xét hoặc chờ sinh viên trả lời.
## Doanh nghiệp đã duyệt [#doanh-nghiệp-đã-duyệt]
`GET /seasons/{id}/companies` trả các doanh nghiệp nhà trường đã duyệt tham gia đợt (không phân trang):
```json
{
"data": [
{
"id": "c8d7e6f5g4h3j2k1l0m9n8b7",
"name": "Công ty Cổ phần Ví dụ",
"shortName": "Ví dụ",
"sponsor": false,
"hiringOpen": true,
"maximumWaitingApplications": 30,
"maximumAcceptedStudents": 10
}
]
}
```
`hiringOpen`: doanh nghiệp còn nhận hồ sơ. Hai số tối đa là `null` khi không giới hạn.
# Giới hạn và lỗi (/thuc-tap-doanh-nghiep/lap-trinh/api-cong/gioi-han-va-loi)
## Giới hạn [#giới-hạn]
Giới hạn bảo vệ cổng của trường khỏi việc gửi dồn dập (theo OWASP API Security, mục API4: tiêu thụ tài nguyên không giới hạn). Các số dưới đây là mặc định cho mỗi kết nối; cần mức khác thì liên hệ nhà trường.
| Giới hạn | Mặc định |
| --- | --- |
| Lời gọi đọc | 600 mỗi phút |
| Lời gọi ghi | 60 mỗi phút |
| Lời gọi cùng lúc | 4 |
| Bản ghi gửi lên | 2.000 mỗi phút, 50.000 mỗi ngày |
| Bản ghi cả trường nhận (mọi kết nối cộng lại) | 5.000 mỗi phút |
| Sinh viên mỗi lần gửi theo lô | 500 |
| Thân JSON | 5 MB |
| Gói CSV OneRoster | 20 MB, 50.000 dòng mỗi tệp |
| Lượt xử lý chờ hoặc đang chạy của một trường | 4 |
Lượt xử lý chạy dần theo giới hạn bản ghi, nên một gói lớn có thể mất vài phút.
## Tiêu đề báo giới hạn [#tiêu-đề-báo-giới-hạn]
Mỗi phản hồi có tiêu đề theo bản thảo IETF *RateLimit header fields for HTTP*:
```http
RateLimit-Policy: "read";q=600;w=60
RateLimit: "read";r=596;t=42
```
- `q`: số lần cho phép trong `w` giây. `r`: số lần còn lại. `t`: số giây tới khi tính lại.
- Lệnh ghi có thêm chính sách `"records"` cho số bản ghi mỗi phút.
Vượt giới hạn trả `429 Too Many Requests` kèm `Retry-After` (số giây). Hãy đợi đúng thời gian đó rồi gửi lại; gửi lệnh ghi kèm `Idempotency-Key` để gửi lại không tạo việc trùng.
## Lỗi [#lỗi]
Lỗi theo Problem Details (RFC 9457), kiểu `application/problem+json`:
```json
{
"type": "urn:ttdn:problem:invalid-request",
"title": "The request is not valid",
"status": 422,
"detail": "Some students are not valid; nothing was queued.",
"instance": "/integration/v1/students/batch",
"errors": [{ "index": 3, "ref": "SV2026004", "message": "studentCode and name are required." }]
}
```
Dựa vào `type` để xử lý; `title` và `detail` (tiếng Anh) để đọc.
| Mã HTTP | `type` (`urn:ttdn:problem:...`) | Khi nào |
| --- | --- | --- |
| 401 | `unauthorized` | Thiếu mã, mã sai, hết hạn, đã thu hồi, hoặc mã của trường khác |
| 403 | `insufficient-scope` | Mã không có quyền cho điểm truy cập này |
| 403 | `ip-not-allowed` | Gọi từ địa chỉ mạng không nằm trong danh sách nhà trường cho phép |
| 404 | `not-found` | Không có đợt, sinh viên hay lượt xử lý này |
| 409 | `conflict`, `code-taken`, `login-name-taken`, `declared-code` | Dữ liệu trỏ tới tài khoản khác (email, mã sinh viên, tên đăng nhập đã thuộc người khác) |
| 409 | `season-closed` | Đưa sinh viên vào đợt đã đóng |
| 413 | `too-large` | Thân yêu cầu quá lớn hoặc quá 500 sinh viên |
| 422 | `invalid-request` | Dữ liệu không hợp lệ; `errors` liệt kê từng dòng |
| 422 | `invalid-package` | Gói CSV OneRoster không đọc được |
| 422 | `idempotency-key-reused`, `invalid-idempotency-key` | `Idempotency-Key` đã dùng cho nội dung khác, hoặc dài quá 255 ký tự |
| 429 | `rate-limited` | Vượt giới hạn; đợi theo `Retry-After` |
| 500 | `internal-error` | Lỗi phía cổng; thử lại sau, vẫn lỗi thì báo nhà trường |
| 503 | `busy` | Trường đã có 4 lượt chờ; đợi theo `Retry-After` |
## Nhà trường thấy gì [#nhà-trường-thấy-gì]
Ở trang **Kết nối hệ thống**, mục **Theo dõi** của mỗi kết nối cho nhà trường thấy số lần gọi mỗi ngày trong 30 ngày (thành công, lỗi do yêu cầu, lỗi hệ thống), số bản ghi đã gửi và các lỗi gần đây. Cổng chỉ đếm số lượng, **không lưu nội dung** yêu cầu. Số liệu theo ngày giữ 13 tháng, lỗi gần đây giữ 30 ngày.
# Gói CSV OneRoster (/thuc-tap-doanh-nghiep/lap-trinh/api-cong/goi-oneroster)
Nhiều phần mềm quản lý đào tạo xuất được **gói CSV OneRoster** (một tệp `.zip`). Cổng nhận gói này theo hai cách:
- Cán bộ nhà trường tải gói lên ở trang **Kết nối hệ thống** (luôn xem trước rồi mới áp dụng).
- Hệ thống của trường gửi qua API: `POST /integration/v1/imports/oneroster`, quyền `imports:write`.
## Gửi qua API [#gửi-qua-api]
Thân yêu cầu là chính tệp zip, `Content-Type: application/zip`, tối đa **20 MB**.
```bash
curl -X POST -H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/zip" \
-H "Idempotency-Key: 2026-11-13-oneroster" \
--data-binary @oneroster.zip \
"https://./integration/v1/imports/oneroster?dryRun=true"
```
Phản hồi `202` kèm một lượt xử lý, theo dõi ở `/jobs/{id}` như [Gửi dữ liệu](./gui-du-lieu.mdx#theo-dõi-việc-xử-lý). Với `?dryRun=true`, `counts` cho biết những gì sẽ thay đổi mà không lưu gì; bỏ `dryRun` để áp dụng thật. `warnings.package` ghi phiên bản gói, chế độ từng tệp, số người không phải sinh viên bị bỏ qua và các lưu ý.
Gói không đọc được trả `422` với `type` là `urn:ttdn:problem:invalid-package` và lý do trong `detail`.
## Gói cần có gì [#gói-cần-có-gì]
| Tệp | Bắt buộc | Cổng dùng |
| --- | --- | --- |
| `manifest.csv` | có | `oneroster.version` (`1.2`, cũng đọc `1.1`), và `file.` = `bulk`, `delta` hoặc `absent` cho từng tệp |
| `users.csv` | có | Người dùng; chỉ lấy người có vai trò sinh viên |
| `roles.csv` | với bản 1.2 | Vai trò của người dùng (bản 1.1 lấy cột `role` trong `users.csv`) |
| `academicSessions.csv` | không | Kỳ học (`term`, `semester`) thành đợt thực tập |
| `classes.csv` | không | Lớp, để biết kỳ của từng ghi danh |
| `enrollments.csv` | không | Ghi danh của sinh viên vào lớp |
Các tệp khác (`orgs.csv`, `courses.csv`, `demographics.csv`...) được bỏ qua. Mỗi tệp có dòng tiêu đề, tối đa 50.000 dòng.
## Cổng hiểu gói thế nào [#cổng-hiểu-gói-thế-nào]
- **Đợt thực tập:** mỗi kỳ học (`term` hoặc `semester`) mà các lớp trong gói dùng thành một đợt, xếp theo ngày như [gửi đợt qua API](./gui-du-lieu.mdx#đợt-thực-tập). Gói không có lớp thì dùng mọi kỳ học.
- **Sinh viên:** mọi người có vai trò `student`. Mã sinh viên lấy theo thứ tự: cột `identifier`, `userIds` dạng `{studentCode:SV2026001}`, rồi `username`. Họ tên ghép theo thứ tự Việt Nam: `familyName middleName givenName`. `sourcedId` thành `externalId`.
- **Cách đăng nhập:** dòng có `password` thì sinh viên đăng nhập bằng tên đăng nhập (`username`, hoặc mã sinh viên) và mật khẩu; không có thì đăng nhập bằng Google theo `email`.
- **Ghi danh:** sinh viên ghi danh vào lớp thì được đưa vào đợt của kỳ của lớp đó. Gói không có `enrollments.csv` thì sinh viên được đưa vào đợt đang mở.
- **Khoá:** dòng có `status = tobedeleted` hoặc `enabledUser = false` thì tài khoản bị khoá.
- **Không xoá:** sinh viên vắng mặt trong tệp `bulk` được giữ nguyên. Kỳ học và ghi danh bị đánh dấu xoá chỉ được ghi chú, không áp dụng.
- Người có vai trò khác (giáo viên, phụ huynh...) được bỏ qua và đếm trong `ignoredUsers`.
Khi kỳ học đang diễn ra mà trường đang mở một đợt khác, cổng tạo đợt mới ở trạng thái chưa mở và báo `switchSeason` trong `warnings`. Cán bộ nhà trường xác nhận chuyển đợt ở trang **Kết nối hệ thống**.
# Gửi dữ liệu (/thuc-tap-doanh-nghiep/lap-trinh/api-cong/gui-du-lieu)
Mọi đường dẫn tính từ `https://./integration/v1`. Thân yêu cầu là JSON (`Content-Type: application/json`), tối đa 5 MB.
| Lệnh | Đường dẫn | Quyền | Xử lý |
| --- | --- | --- | --- |
| `PUT` | `/seasons/external/{externalId}` | `seasons:write` | Ngay |
| `PUT` | `/students/{studentCode}` | `students:write` | Ngay |
| `POST` | `/students/batch` | `students:write` | Theo lượt (`202`) |
| `POST` | `/students/{studentCode}/block` | `students:write` | Ngay |
| `POST` | `/students/{studentCode}/unblock` | `students:write` | Ngay |
| `POST` | `/seasons/{id}/enrollments` | `enrollments:write` | Theo lượt (`202`) |
| `GET` | `/jobs/{id}` | một quyền ghi | |
| `GET` | `/jobs/{id}/failures` | một quyền ghi | |
Lệnh gửi theo lô luôn được **xếp lượt** và xử lý dần ở nền, để một lần gửi lớn không làm chậm cổng. Mọi lệnh ghi nhận `?dryRun=true` để chạy thử.
## Nguyên tắc [#nguyên-tắc]
- **Không xoá.** API không xoá sinh viên hay đợt. Sinh viên không còn học thì **khoá**; sinh viên không có trong lần gửi thì giữ nguyên.
- **Không đổi cách đăng nhập của tài khoản đã có.** Cập nhật chỉ đổi họ tên, mã sinh viên, `externalId`; email, mật khẩu và cách đăng nhập của tài khoản đã có giữ nguyên.
- **Mọi sinh viên gửi lên đều là sinh viên thực tập** của trường.
- Mọi thay đổi ghi vào **Nhật ký** của trường, người thực hiện là tên kết nối.
## Đợt thực tập [#đợt-thực-tập]
`PUT /seasons/external/{externalId}` tạo hoặc cập nhật đợt theo một kỳ học bên hệ thống của trường. `externalId` là mã kỳ học (tối đa 128 ký tự).
```bash
curl -X PUT -H "Authorization: Bearer ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Học kỳ 2 năm học 2026-2027","startDate":"2027-02-10","endDate":"2027-06-30"}' \
https://./integration/v1/seasons/external/HK2-2026
```
| Trường | Bắt buộc | Ghi chú |
| --- | --- | --- |
| `name` | có | 2 đến 80 ký tự |
| `startDate`, `endDate` | không | `YYYY-MM-DD`; `endDate` không trước `startDate` |
Cổng xếp đợt theo ngày của kỳ:
| Kỳ học | Đợt mới tạo |
| --- | --- |
| Đã kết thúc | Đợt **đã đóng** (để lưu lịch sử) |
| Đang diễn ra, trường chưa mở đợt nào | Đợt **đang mở**, tự đóng cuối ngày `endDate` |
| Đang diễn ra, trường đang mở đợt khác | Đợt **chưa mở**; phản hồi có `switchNeeded` |
| Chưa bắt đầu | Đợt **chưa mở** |
Cổng **không tự đóng** đợt đang mở để chuyển sang đợt mới: khi có `switchNeeded`, cán bộ nhà trường xác nhận chuyển đợt ở trang **Kết nối hệ thống**. Gửi lại cùng `externalId` thì cập nhật tên và ngày đóng. Đợt chưa mở được mở khi bạn gửi lại trong thời gian kỳ diễn ra và trường không mở đợt nào khác (cổng không tự mở theo lịch).
```json
{
"result": "created",
"data": { "id": "k3v9x2m1q8w7e6r5t4y3u2i1", "externalId": "HK2-2026", "name": "Học kỳ 2 năm học 2026-2027", "status": "upcoming", "openedAt": null, "closesAt": "2027-06-30T16:59:59.000Z", "closedAt": null, "updatedAt": "2026-11-13T02:00:00.000Z" },
"switchNeeded": { "from": "Đợt thực tập học kỳ 1 năm học 2026-2027", "to": "Học kỳ 2 năm học 2026-2027" }
}
```
`result`: `created`, `opened`, `updated`, `unchanged`. Mã `201` khi tạo mới, `200` khi cập nhật.
## Một sinh viên [#một-sinh-viên]
`PUT /students/{studentCode}` tạo hoặc cập nhật một sinh viên (không đổi đợt).
| Trường | Bắt buộc | Ghi chú |
| --- | --- | --- |
| `name` | có | Họ và tên, tối đa 120 ký tự |
| `email` | khi không có `password` | Tài khoản Google của sinh viên; tối đa 254 ký tự |
| `password` | không | 8 đến 128 ký tự. Có mật khẩu thì sinh viên đăng nhập bằng tên đăng nhập |
| `username` | không | Tên đăng nhập (3 đến 64 chữ, số, `.`, `-`, `_`); chỉ dùng kèm `password`. Bỏ trống thì dùng mã sinh viên |
| `externalId` | không | Mã sinh viên bên hệ thống trường, tối đa 128 ký tự |
```bash
curl -X PUT -H "Authorization: Bearer ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Nguyễn Văn An","email":"an.nv@truong.edu.vn","externalId":"U-1001"}' \
https://./integration/v1/students/SV2026001
```
```json
{ "result": "created", "data": { "id": "p0o9i8u7y6t5r4e3w2q1a2s3", "studentCode": "SV2026001", "externalId": "U-1001", "name": "Nguyễn Văn An", "email": "an.nv@truong.edu.vn", "signIn": "google", "loginName": null, "status": "active", "createdAt": "2026-11-13T02:00:00.000Z", "updatedAt": "2026-11-13T02:00:00.000Z" } }
```
`result`: `created` (`201`), `updated`, `unchanged`. Cổng tìm sinh viên theo `externalId`, rồi mã sinh viên, rồi email. Trả `409` khi dữ liệu trỏ tới hai tài khoản khác nhau, ví dụ email đã thuộc sinh viên có mã khác, hoặc mã sinh viên đã là tên đăng nhập của người khác.
## Nhiều sinh viên [#nhiều-sinh-viên]
`POST /students/batch` nhận tối đa **500** sinh viên mỗi lần, cùng các trường như trên cộng `studentCode`. Gửi thêm `season` (là `id` của đợt) để đưa luôn các sinh viên vào đợt đó.
```bash
curl -X POST -H "Authorization: Bearer ACCESS_TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a0e-6d55-4a8e-9a57-0f3b8e7d2c11" \
-d '{"season":"k3v9x2m1q8w7e6r5t4y3u2i1","students":[{"studentCode":"SV2026001","name":"Nguyễn Văn An","email":"an.nv@truong.edu.vn"},{"studentCode":"SV2026002","name":"Trần Thị Bình","password":"MatKhau@2026"}]}' \
https://./integration/v1/students/batch
```
Cổng kiểm tra cả lô trước: có dòng sai (thiếu trường, trùng mã trong lô) thì trả `422` với danh sách `errors` và **không xếp lượt** gì. Hợp lệ thì trả `202 Accepted`, tiêu đề `Location` trỏ tới lượt xử lý:
```json
{ "job": { "id": "at8w3w2oxfpggp9ng0w5a3de", "kind": "students", "status": "queued", "dryRun": false, "total": 2, "processed": 0, "counts": {}, "warnings": {}, "failureCount": 0, "createdAt": "2026-11-13T02:00:00.000Z", "startedAt": null, "finishedAt": null, "by": "Kết nối “Phần mềm đào tạo”" } }
```
## Đưa sinh viên vào đợt [#đưa-sinh-viên-vào-đợt]
`POST /seasons/{id}/enrollments` nhận tối đa 500 sinh viên đã có, mỗi mục là `{ "studentCode": "..." }` hoặc `{ "externalId": "..." }`. Đợt đã đóng trả `409`. Kết quả là một lượt xử lý (`202`), như trên.
## Khoá và mở khoá [#khoá-và-mở-khoá]
`POST /students/{studentCode}/block` khoá tài khoản, như khi nhà trường khoá trên màn hình: sinh viên không đăng nhập được, hồ sơ đang chờ xét hoặc chờ trả lời bị rút và doanh nghiệp được báo. `POST /students/{studentCode}/unblock` mở lại. Không cần thân yêu cầu; phản hồi là sinh viên sau khi đổi.
## Theo dõi việc xử lý [#theo-dõi-việc-xử-lý]
`GET /jobs/{id}` trả trạng thái lượt (chỉ lượt do chính kết nối gửi):
| `status` | Ý nghĩa |
| --- | --- |
| `queued` | Đang chờ |
| `running` | Đang xử lý; `processed` / `total` cho biết tiến độ |
| `succeeded` | Xong (có thể có dòng lỗi, xem `failureCount`) |
| `failed` | Không chạy được |
| `cancelled` | Nhà trường đã huỷ |
`counts` đếm kết quả, ví dụ `students.created`, `students.updated`, `students.blocked`, `enrollments.enrolled`, `seasons.created`, `failed`. Nên hỏi lại mỗi 2 đến 5 giây.
`GET /jobs/{id}/failures` trả các dòng lỗi dạng CSV (`ref,code,message`). Dòng lỗi được giữ 30 ngày.
# Tổng quan API của cổng (/thuc-tap-doanh-nghiep/lap-trinh/api-cong)
Không phải hệ thống nào của trường cũng hỗ trợ OneRoster. **API của cổng (v1)** là một API REST JSON đơn giản để phần mềm của trường **đọc** và **gửi** dữ liệu thực tập: sinh viên, đợt thực tập, ghi danh vào đợt, khoá tài khoản; đọc kết quả thực tập và doanh nghiệp đã duyệt. API này cũng nhận **gói CSV OneRoster** cho trường xuất được gói nhưng không có API OneRoster.
Hệ thống đã hỗ trợ OneRoster 1.2 thì nên dùng [API OneRoster](../oneroster/index.mdx) để đọc. Hai API dùng chung kết nối, quyền và giới hạn.
## Đặc điểm chính [#đặc-điểm-chính]
- **Địa chỉ gốc:** `https://./integration/v1`. Trường được xác định theo tên miền; mã của trường nào chỉ dùng được ở trường đó.
- **Định dạng:** JSON UTF-8. Thời điểm theo ISO 8601 (UTC); ngày (`startDate`, `endDate`) dạng `YYYY-MM-DD` theo giờ Việt Nam.
- **Lỗi:** theo Problem Details (RFC 9457), kiểu `application/problem+json`. Xem [Giới hạn và lỗi](./gioi-han-va-loi.mdx).
- **Đặc tả máy đọc được:** OpenAPI 3.1 tại `/integration/v1/openapi.json`.
## Xác thực [#xác-thực]
Mọi lời gọi gửi tiêu đề `Authorization: Bearer `. Nhà trường tạo kết nối ở trang **Kết nối hệ thống** và chọn một trong hai loại:
| Loại kết nối | Cách dùng | Phù hợp |
| --- | --- | --- |
| **Mã truy cập** | Một mã dạng `ttdn_pat_...` gửi thẳng trong mỗi lời gọi. Có thể đặt thời hạn (30, 90, 180, 365 ngày) hoặc không hết hạn. | Hệ thống đơn giản, kịch bản đồng bộ |
| **Mã ứng dụng và khoá bí mật** | Lấy mã 1 giờ ở `/oauth/token` theo OAuth 2.0 Client Credentials, như [OneRoster](../oneroster/xac-thuc.mdx). | Hệ thống đã quen OAuth 2.0 |
```bash
curl -H "Authorization: Bearer ACCESS_TOKEN" \
"https://./integration/v1/seasons"
```
- **Thay khoá:** nhà trường bấm **Thay khoá** để cấp mã (hoặc khoá bí mật) mới; mã cũ còn dùng được **24 giờ** để kịp đổi.
- **Thu hồi:** ngừng ngay mọi mã của kết nối; không hoàn tác được.
- **Giới hạn địa chỉ:** nếu nhà trường nhập danh sách địa chỉ mạng (IPv4/IPv6, nhận dạng CIDR như `203.0.113.0/24`), lời gọi từ địa chỉ khác bị từ chối với `403`.
Mã truy cập và khoá bí mật chỉ hiện một lần lúc tạo. Lưu ở biến môi trường hay kho khoá, không đưa vào mã nguồn, không gửi qua tin nhắn không mã hoá.
## Quyền [#quyền]
| Quyền (scope) | Cho phép |
| --- | --- |
| `students:read` | Đọc sinh viên |
| `students:write` | Thêm, sửa, khoá, mở khoá sinh viên |
| `seasons:read` | Đọc đợt thực tập |
| `seasons:write` | Tạo, cập nhật đợt theo kỳ của hệ thống trường |
| `enrollments:read` | Đọc sinh viên trong đợt |
| `enrollments:write` | Đưa sinh viên vào đợt |
| `results:read` | Đọc kết quả thực tập |
| `companies:read` | Đọc doanh nghiệp đã duyệt trong đợt |
| `imports:write` | Gửi gói CSV OneRoster |
Theo dõi việc xử lý (`/jobs/...`) cần một trong các quyền ghi: `students:write`, `enrollments:write`, `imports:write`. Thiếu quyền trả `403` kèm tên quyền cần.
## Quy ước chung [#quy-ước-chung]
### Phân trang [#phân-trang]
Danh sách dài (`/students`, `/seasons/{id}/enrollments`, `/seasons/{id}/results`) trả từng trang:
- `limit`: 1 đến 500, mặc định 100.
- Thân phản hồi có `next`: chuỗi con trỏ của trang sau, hoặc `null` khi hết. Gửi lại bằng `?cursor=` cùng các tham số cũ.
- Tiêu đề `Link: <...>; rel="next"` (RFC 8288) chứa sẵn địa chỉ trang sau.
### Đồng bộ phần thay đổi [#đồng-bộ-phần-thay-đổi]
`GET /students?updatedSince=2026-11-01T00:00:00Z` chỉ trả sinh viên có thay đổi từ thời điểm đó. Lưu thời điểm bắt đầu mỗi lần đồng bộ rồi dùng cho lần sau.
### Chạy thử [#chạy-thử]
Mọi lệnh ghi nhận `?dryRun=true`: cổng kiểm tra và trả kết quả như thật nhưng **không lưu gì**.
### Gửi lại an toàn [#gửi-lại-an-toàn]
Lệnh gửi theo lô (`POST`) nhận tiêu đề `Idempotency-Key` (1 đến 255 ký tự, nên dùng UUID). Gửi lại cùng khoá trong **24 giờ** thì nhận lại đúng phản hồi lần đầu (kèm `Idempotent-Replayed: true`), không tạo việc mới. Cùng khoá mà nội dung khác thì bị từ chối (`422`).
### Định danh [#định-danh]
- Sinh viên được nhận ra theo **mã sinh viên** (`studentCode`, không phân biệt hoa thường).
- `externalId` (tuỳ chọn, tối đa 128 ký tự) là mã của bản ghi trong hệ thống của trường. Gửi kèm thì lần sau cổng nhận ra sinh viên theo mã này trước, rồi mới tới mã sinh viên, rồi email.
- Đợt thực tập có `id` do cổng cấp và `externalId` là mã kỳ học bên hệ thống trường.
## Các nhóm điểm truy cập [#các-nhóm-điểm-truy-cập]
| Nhóm | Trang |
| --- | --- |
| Đọc đợt, sinh viên, ghi danh, kết quả, doanh nghiệp | [Đọc dữ liệu](./doc-du-lieu.mdx) |
| Gửi đợt, sinh viên, ghi danh, khoá; theo dõi việc xử lý | [Gửi dữ liệu](./gui-du-lieu.mdx) |
| Gửi gói CSV OneRoster | [Gói CSV OneRoster](./goi-oneroster.mdx) |
# Dữ liệu danh sách (/thuc-tap-doanh-nghiep/lap-trinh/oneroster/du-lieu)
Địa chỉ gốc: `/ims/oneroster/rostering/v1p2`. Cần quyền "Danh sách" (`roster.readonly` hoặc `roster-core.readonly`).
## Điểm truy cập [#điểm-truy-cập]
Mọi danh sách đều có dạng lấy một bản ghi: `/{tên}/{sourcedId}`, trả về đối tượng số ít (ví dụ `user`).
| Đường dẫn | Nội dung |
| --- | --- |
| `GET /orgs`, `/schools` | Trường |
| `GET /academicSessions`, `/terms`, `/schools/{id}/terms` | Các đợt thực tập |
| `GET /courses`, `/schools/{id}/courses` | Học phần "Thực tập doanh nghiệp" |
| `GET /classes`, `/terms/{id}/classes`, `/schools/{id}/classes`, `/courses/{id}/classes` | Mỗi đợt một lớp |
| `GET /users/{id}/classes`, `/students/{id}/classes` | Các đợt (lớp) một sinh viên có tên |
| `GET /users`, `/students`, `/schools/{id}/students` | Sinh viên và cán bộ trường |
| `GET /classes/{id}/students` | Sinh viên của một đợt |
| `GET /enrollments`, `/classes/{id}/enrollments`, `/schools/{id}/enrollments` | Sinh viên có tên trong đợt |
### Luôn rỗng [#luôn-rỗng]
Nền tảng không có các đối tượng sau, nên các điểm truy cập chuẩn này luôn trả danh sách rỗng (mã 200), và lấy một bản ghi theo mã thì trả 404:
- `GET /teachers`, `/schools/{id}/teachers`, `/classes/{id}/teachers`, `/teachers/{id}/classes`: không có giáo viên.
- `GET /gradingPeriods`, `/terms/{id}/gradingPeriods`: đợt thực tập không chia kỳ chấm điểm.
- `GET /demographics`: nền tảng không lưu dữ liệu nhân khẩu.
## Ánh xạ nghiệp vụ [#ánh-xạ-nghiệp-vụ]
| Nghiệp vụ | OneRoster |
| --- | --- |
| Trường | `orgs` (`type = school`); `identifier` là mã trường trên nền tảng |
| Đợt thực tập | `academicSessions` (`type = term`); `startDate` là ngày mở, `endDate` là ngày đóng hoặc ngày dự kiến đóng; `metadata."ttdn.open"` cho biết đợt đang mở |
| Học phần thực tập | `courses` (một học phần, mã `TTDN`) và `classes` (mỗi đợt một lớp) |
| Sinh viên | `users` với `roles[].role = student`; mã sinh viên ở `identifier` và `userIds[type = studentCode]`; `email` là `null` khi sinh viên không có email thật |
| Tên đăng nhập | `username`: tên đăng nhập hoặc mã sinh viên với tài khoản dùng mật khẩu; email với tài khoản Google |
| Họ tên | `familyName` là họ, `middleName` là tên đệm, `givenName` là tên |
| Cán bộ trường | `users` với `roles[].role = administrator` |
| Tài khoản bị khoá | `enabledUser = false` |
| Tài khoản đã gỡ | `status = tobedeleted`, vẫn được liệt kê để việc đồng bộ biết mà cập nhật |
| Sinh viên có tên trong đợt | `enrollments` (`role = student`) vào lớp của đợt; `beginDate` là ngày sinh viên được đưa vào đợt, `endDate` là ngày đóng (hoặc dự kiến đóng) của đợt |
Doanh nghiệp không có vai trò tương ứng trong OneRoster nên không xuất hiện trong `users`; tên doanh nghiệp đi kèm [kết quả thực tập](./ket-qua.mdx).
Trường `metadata` của sinh viên có thêm:
- `ttdn.selfSignup`: sinh viên tự tạo tài khoản bằng Google của trường.
- `ttdn.studentCodeDeclared`: mã sinh viên do sinh viên tự khai, nhà trường chưa xác nhận.
## Ví dụ [#ví-dụ]
```json
{
"users": [
{
"sourcedId": "k3x9q2m7p1v8w4z6",
"status": "active",
"dateLastModified": "2026-11-10T08:15:00.000Z",
"enabledUser": true,
"username": "SV2026001",
"userIds": [{ "type": "studentCode", "identifier": "SV2026001" }],
"givenName": "An",
"familyName": "Nguyễn",
"middleName": "Văn",
"roles": [{ "roleType": "primary", "role": "student", "org": { "href": "/ims/oneroster/rostering/v1p2/orgs/…", "sourcedId": "…", "type": "org" } }],
"identifier": "SV2026001",
"email": "an.nv@truong.edu.vn",
"metadata": { "ttdn.selfSignup": false, "ttdn.studentCodeDeclared": false }
}
]
}
```
# Tổng quan OneRoster (/thuc-tap-doanh-nghiep/lap-trinh/oneroster)
Mỗi cổng thực tập của một trường mở một API theo chuẩn quốc tế **1EdTech OneRoster 1.2**, để phần mềm quản lý đào tạo (SIS) hay hệ thống khác của trường đọc danh sách sinh viên, đợt thực tập và kết quả thực tập. Phần mềm nào đã hỗ trợ OneRoster đều kết nối được mà không cần viết riêng.
## Đặc điểm chính [#đặc-điểm-chính]
- **Chuẩn:** OneRoster 1.2, phần Rostering (danh sách) và Gradebook (kết quả), giao thức REST, chỉ đọc.
- **Xác thực:** OAuth 2.0, luồng Client Credentials, đúng như OneRoster 1.2 yêu cầu. Xem [Xác thực](./xac-thuc.mdx).
- **Địa chỉ:** mỗi trường dùng chính địa chỉ cổng của trường, ví dụ `https://.`. Thông tin xác thực của trường nào chỉ dùng được ở trường đó và chỉ thấy dữ liệu của trường đó.
- **Định dạng:** JSON, mã hoá UTF-8. Thời điểm theo ISO 8601 (UTC); ngày (`startDate`, `endDate`, `scoreDate`) theo giờ Việt Nam.
## Bắt đầu nhanh [#bắt-đầu-nhanh]
### Tạo kết nối [#tạo-kết-nối]
Cán bộ nhà trường vào **Kết nối hệ thống** → **Tạo kết nối**, đặt tên, chọn quyền, rồi gửi mã ứng dụng (`client_id`) và khoá bí mật (`client_secret`) cho đơn vị phụ trách hệ thống. Khoá bí mật chỉ hiện một lần.
### Lấy mã truy cập [#lấy-mã-truy-cập]
```bash
curl -u "CLIENT_ID:CLIENT_SECRET" \
-d "grant_type=client_credentials" \
https://./oauth/token
```
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "https://purl.imsglobal.org/spec/or/v1p2/scope/roster.readonly https://purl.imsglobal.org/spec/or/v1p2/scope/gradebook.readonly"
}
```
### Gọi API [#gọi-api]
```bash
curl -H "Authorization: Bearer ACCESS_TOKEN" \
"https://./ims/oneroster/rostering/v1p2/students?limit=100"
```
## Hai nhóm API [#hai-nhóm-api]
| Nhóm | Địa chỉ gốc | Nội dung |
| --- | --- | --- |
| Danh sách (Rostering) | `/ims/oneroster/rostering/v1p2` | Trường, đợt thực tập, học phần, lớp, sinh viên, cán bộ trường, ghi danh |
| Kết quả (Gradebook) | `/ims/oneroster/gradebook/v1p2` | Kết quả thực tập của từng sinh viên trong từng đợt |
Chi tiết từng nhóm: [Dữ liệu danh sách](./du-lieu.mdx) và [Kết quả thực tập](./ket-qua.mdx). Mã mẫu chạy được: [Ví dụ đồng bộ](./vi-du.mdx).
## Gửi dữ liệu vào cổng [#gửi-dữ-liệu-vào-cổng]
API OneRoster chỉ đọc. Để hệ thống của trường gửi sinh viên, đợt thực tập, ghi danh vào cổng, có hai cách:
| Cách | Dùng khi |
| --- | --- |
| [Gói CSV OneRoster](../api-cong/goi-oneroster.mdx) | Phần mềm của trường xuất được gói CSV OneRoster |
| [API của cổng (v1)](../api-cong/index.mdx) | Hệ thống không theo OneRoster; cần API JSON đơn giản để đọc và gửi |
## Đặc tả máy đọc được [#đặc-tả-máy-đọc-được]
Mỗi cổng có tệp OpenAPI 3 tại `/ims/oneroster/openapi.json`, nhập được vào Postman hoặc dùng để sinh mã. Bản đặc tả đầy đủ của chuẩn có tại [trang 1EdTech](https://www.1edtech.org/standards/oneroster).
# Kết quả thực tập (/thuc-tap-doanh-nghiep/lap-trinh/oneroster/ket-qua)
Địa chỉ gốc: `/ims/oneroster/gradebook/v1p2`. Cần quyền "Kết quả thực tập" (`gradebook.readonly`).
## Điểm truy cập [#điểm-truy-cập]
| Đường dẫn | Nội dung |
| --- | --- |
| `GET /categories` | Một hạng mục "Thực tập" |
| `GET /lineItems`, `/classes/{id}/lineItems` | Mỗi đợt một cột "Kết quả thực tập"; `assignDate` là ngày mở đợt, `dueDate` là ngày đóng |
| `GET /results`, `/lineItems/{id}/results`, `/classes/{id}/results` | Mỗi sinh viên trong mỗi đợt một kết quả |
| `GET /scoreScales`, `/classes/{id}/scoreScales`, `/assessmentLineItems`, `/assessmentResults` | Luôn rỗng: nền tảng không chấm điểm |
## Tình trạng thực tập [#tình-trạng-thực-tập]
| Tình trạng | `scoreStatus` | `textScore` |
| --- | --- | --- |
| Đã được doanh nghiệp nhận | `fully graded` | Đã có nơi thực tập |
| Chưa có nơi, còn hồ sơ đang chờ doanh nghiệp trả lời | `submitted` | Chưa có nơi thực tập |
| Chưa có nơi, không có hồ sơ nào đang chờ | `not submitted` | Chưa có nơi thực tập |
Khi sinh viên đã được nhận:
- `comment` là tên doanh nghiệp, `scoreDate` là ngày được nhận.
- `metadata` có `ttdn.placed`, `ttdn.companyId`, `ttdn.companyName` và `ttdn.acceptedAt`.
Khi chưa có nơi, `metadata` có `ttdn.placed = false` và `ttdn.pendingApplications` (còn hồ sơ đang chờ hay không). `score` luôn để trống vì nền tảng không chấm điểm thực tập.
Doanh nghiệp nhận sinh viên là quyết định cuối cùng: sinh viên có nơi thực tập ngay, không cần xác nhận thêm. Vì vậy `fully graded` không đổi lại về trạng thái khác.
## Ví dụ [#ví-dụ]
```http
GET /ims/oneroster/gradebook/v1p2/results?filter=scoreStatus='fully graded'
```
```json
{
"results": [
{
"sourcedId": "e7c2…-result",
"status": "active",
"dateLastModified": "2026-11-18T02:30:00.000Z",
"lineItem": { "href": "/ims/oneroster/gradebook/v1p2/lineItems/…", "sourcedId": "…-result", "type": "lineItem" },
"student": { "href": "/ims/oneroster/rostering/v1p2/users/…", "sourcedId": "k3x9q2m7p1v8w4z6", "type": "user" },
"class": { "href": "/ims/oneroster/rostering/v1p2/classes/…", "sourcedId": "…-class", "type": "class" },
"scoreStatus": "fully graded",
"score": null,
"textScore": "Đã có nơi thực tập",
"scoreDate": "2026-11-18",
"comment": "Công ty Cổ phần ACME",
"metadata": { "ttdn.placed": true, "ttdn.companyId": "…", "ttdn.companyName": "Công ty Cổ phần ACME", "ttdn.acceptedAt": "2026-11-18T02:30:00.000Z" }
}
]
}
```
# Lỗi và giới hạn (/thuc-tap-doanh-nghiep/lap-trinh/oneroster/loi-va-gioi-han)
## Lỗi [#lỗi]
Lỗi trả về theo khuôn trạng thái của OneRoster:
```json
{
"imsx_codeMajor": "failure",
"imsx_severity": "error",
"imsx_description": "\"nope\" cannot be filtered on.",
"imsx_CodeMinor": { "imsx_codeMinorField": [{ "imsx_codeMinorFieldName": "TargetEndSystem", "imsx_codeMinorFieldValue": "invalid_filter_field" }] }
}
```
| Mã HTTP | `imsx_codeMinorFieldValue` | Khi nào |
| --- | --- | --- |
| 400 | `invalid_filter_field`, `invalid_sort_field`, `invalid_selection_field`, `invalid_data` | Tham số không hợp lệ |
| 401 | `unauthorisedrequest` | Thiếu mã, mã sai hoặc hết hạn, kết nối đã thu hồi, mã của trường khác |
| 403 | `forbidden` | Mã không có quyền cho điểm truy cập này |
| 404 | `unknownobject` | Không có điểm truy cập hoặc bản ghi này |
| 429 | `server_busy` | Gọi quá 600 lần trong một phút; đợi rồi thử lại |
| 500 | `internal_server_error` | Lỗi phía hệ thống; thử lại sau, vẫn lỗi thì báo nhà trường |
| 503 | (không có) | Hệ thống tạm thời không phản hồi; thử lại sau ít phút |
Lỗi khi lấy mã theo OAuth 2.0, xem [Xác thực](./xac-thuc.mdx#lỗi-khi-lấy-mã).
## Giới hạn [#giới-hạn]
- 600 yêu cầu mỗi phút cho mỗi kết nối.
- Lấy mã: 60 lần mỗi 15 phút cho mỗi địa chỉ mạng.
- Mỗi trường có tối đa 10 kết nối đang hoạt động.
## Chưa hỗ trợ [#chưa-hỗ-trợ]
- Ghi qua REST OneRoster: theo chuẩn, phần Danh sách không có lệnh ghi; phần Kết quả cũng không nhận ghi vì kết quả thực tập do chính nền tảng tạo ra. Để gửi dữ liệu vào cổng, dùng [gói CSV OneRoster](../api-cong/goi-oneroster.mdx) hoặc [API của cổng](../api-cong/gui-du-lieu.mdx).
- Dịch vụ Resources và người giám hộ. Giáo viên, kỳ chấm điểm, dữ liệu nhân khẩu có điểm truy cập nhưng luôn rỗng.
- Thông báo sự kiện (webhook).
# Truy vấn và đồng bộ (/thuc-tap-doanh-nghiep/lap-trinh/oneroster/truy-van)
## Tham số [#tham-số]
| Tham số | Ý nghĩa | Ví dụ |
| --- | --- | --- |
| `limit`, `offset` | Phân trang; `limit` từ 1 đến 1000, mặc định 100 | `limit=200&offset=400` |
| `filter` | Lọc theo trường: `=` `!=` `>` `>=` `<` `<=` `~` (chứa); nối bằng `AND` hoặc `OR`; giá trị trong dấu nháy đơn; trường lồng nhau dùng dấu chấm | `filter=identifier='SV2026001'` |
| `sort`, `orderBy` | Sắp xếp theo một trường, `asc` hoặc `desc`; chữ theo thứ tự bảng chữ cái tiếng Việt (Đ sau D), số trong chữ theo giá trị (SV9 trước SV10) | `sort=familyName&orderBy=asc` |
| `fields` | Chỉ trả về các trường liệt kê | `fields=sourcedId,identifier,email` |
Lưu ý:
- Mã hoá giá trị trong địa chỉ, ví dụ `filter=identifier%3D'SV2026001'`.
- Một bộ lọc chỉ dùng toàn `AND` hoặc toàn `OR`, không trộn hai loại.
- `~` không phân biệt chữ hoa, chữ thường.
- Lọc hoặc sắp xếp theo trường không có trả lỗi 400. `sort` chỉ dùng được trường ở cấp ngoài cùng (ví dụ `familyName`, không phải `roles.role`); `filter` dùng được trường lồng nhau.
## Phân trang [#phân-trang]
Phản hồi danh sách có hai tiêu đề:
- `X-Total-Count`: tổng số bản ghi khớp điều kiện.
- `Link`: địa chỉ các trang `first`, `prev`, `next`, `last`.
```http
X-Total-Count: 1250
Link: ; rel="next", …
```
## Đồng bộ phần thay đổi [#đồng-bộ-phần-thay-đổi]
Mỗi bản ghi có `dateLastModified`. Để chỉ lấy những gì đổi từ lần đồng bộ trước, lưu thời điểm bắt đầu lần đồng bộ đó rồi lọc:
```http
GET /ims/oneroster/rostering/v1p2/users?filter=dateLastModified>'2026-11-10T00:00:00.000Z'
```
Bản ghi đã gỡ vẫn được trả về với `status = tobedeleted` để hệ thống của trường cập nhật theo. Nền tảng không xoá hẳn dữ liệu.
## Gợi ý cho hệ thống của trường [#gợi-ý-cho-hệ-thống-của-trường]
- Đồng bộ đầy đủ một lần, sau đó đồng bộ phần thay đổi theo lịch (ví dụ mỗi giờ).
- Dùng `sourcedId` để đối chiếu bản ghi giữa hai hệ thống; mã này không đổi.
- Dùng `fields` để chỉ lấy những gì cần, giảm dung lượng.
# Ví dụ đồng bộ (/thuc-tap-doanh-nghiep/lap-trinh/oneroster/vi-du)
Ví dụ dưới đây làm đủ một vòng đồng bộ: lấy mã truy cập, đọc từng trang danh sách sinh viên, rồi đọc kết quả thực tập. Thay `BASE` bằng địa chỉ cổng của trường, đặt mã ứng dụng và khoá bí mật vào biến môi trường `TTDN_CLIENT_ID`, `TTDN_CLIENT_SECRET`. Không ghi khoá bí mật thẳng vào mã.
```python
import os
import requests
BASE = "https://truong.example.edu.vn" # địa chỉ cổng của trường
CLIENT_ID = os.environ["TTDN_CLIENT_ID"]
CLIENT_SECRET = os.environ["TTDN_CLIENT_SECRET"]
def get_token():
r = requests.post(
f"{BASE}/oauth/token",
auth=(CLIENT_ID, CLIENT_SECRET),
data={"grant_type": "client_credentials"},
timeout=30,
)
r.raise_for_status()
return r.json()["access_token"] # hết hạn sau 1 giờ
def read_all(token, path, key, params=None):
"""Đọc hết mọi trang của một danh sách, 1000 bản ghi mỗi lần."""
rows, offset = [], 0
while True:
r = requests.get(
f"{BASE}/ims/oneroster/{path}",
headers={"Authorization": f"Bearer {token}"},
params={**(params or {}), "limit": 1000, "offset": offset},
timeout=30,
)
r.raise_for_status()
page = r.json()[key]
rows += page
total = int(r.headers.get("X-Total-Count", len(rows)))
offset += len(page)
if not page or offset >= total:
return rows
token = get_token()
students = read_all(token, "rostering/v1p2/students", "users")
results = read_all(token, "gradebook/v1p2/results", "results")
placed = {r["student"]["sourcedId"]: r["comment"] for r in results if r["scoreStatus"] == "fully graded"}
for s in students:
print(s["identifier"], s["familyName"], s["givenName"], "→", placed.get(s["sourcedId"], "chưa có nơi thực tập"))
```
Lưu thành tệp `.mjs` (dùng `await` ở cấp ngoài cùng).
```js
const BASE = "https://truong.example.edu.vn"; // địa chỉ cổng của trường
const { TTDN_CLIENT_ID: id, TTDN_CLIENT_SECRET: secret } = process.env;
async function getToken() {
const res = await fetch(`${BASE}/oauth/token`, {
method: "POST",
headers: {
authorization: "Basic " + Buffer.from(`${id}:${secret}`).toString("base64"),
"content-type": "application/x-www-form-urlencoded",
},
body: "grant_type=client_credentials",
});
if (!res.ok) throw new Error(`token ${res.status}`);
return (await res.json()).access_token; // hết hạn sau 1 giờ
}
/** Đọc hết mọi trang của một danh sách, 1000 bản ghi mỗi lần. */
async function readAll(token, path, key, params = {}) {
const rows = [];
for (let offset = 0; ; ) {
const query = new URLSearchParams({ ...params, limit: "1000", offset: String(offset) });
const res = await fetch(`${BASE}/ims/oneroster/${path}?${query}`, { headers: { authorization: `Bearer ${token}` } });
if (!res.ok) throw new Error(`${path} ${res.status}`);
const page = (await res.json())[key];
rows.push(...page);
offset += page.length;
const total = Number(res.headers.get("x-total-count") ?? rows.length);
if (page.length === 0 || offset >= total) return rows;
}
}
const token = await getToken();
const students = await readAll(token, "rostering/v1p2/students", "users");
const results = await readAll(token, "gradebook/v1p2/results", "results");
const placed = new Map(results.filter((r) => r.scoreStatus === "fully graded").map((r) => [r.student.sourcedId, r.comment]));
for (const s of students) console.log(s.identifier, s.familyName, s.givenName, "→", placed.get(s.sourcedId) ?? "chưa có nơi thực tập");
```
## Đồng bộ định kỳ [#đồng-bộ-định-kỳ]
Sau lần đầu, chỉ cần lấy phần thay đổi. Lưu lại thời điểm **bắt đầu** mỗi lần chạy và truyền vào bộ lọc ở lần sau:
```python
since = "2026-11-10T00:00:00.000Z" # thời điểm bắt đầu lần chạy trước
changed = read_all(token, "rostering/v1p2/users", "users", {"filter": f"dateLastModified>'{since}'"})
```
Bản ghi có `status = "tobedeleted"` là bản ghi đã gỡ ở cổng thực tập: cập nhật hoặc ẩn ở hệ thống của trường.
## Xử lý lỗi [#xử-lý-lỗi]
- `401`: mã hết hạn hoặc kết nối đã bị thu hồi. Xin mã mới; vẫn lỗi thì liên hệ nhà trường.
- `429`: gọi quá 600 lần trong một phút. Đợi khoảng một phút rồi thử lại.
- Các lỗi khác: xem [Lỗi và giới hạn](./loi-va-gioi-han.mdx).
# Xác thực (/thuc-tap-doanh-nghiep/lap-trinh/oneroster/xac-thuc)
## Lấy mã truy cập [#lấy-mã-truy-cập]
- **Địa chỉ:** `POST https://./oauth/token`
- **Dạng gửi:** `application/x-www-form-urlencoded` (khuyên dùng, theo OAuth 2.0) hoặc JSON, với `grant_type=client_credentials`.
- **Thông tin kết nối:** gửi `client_id` và `client_secret` bằng HTTP Basic (khuyên dùng), hoặc trong thân yêu cầu.
- **`scope`** (không bắt buộc): các quyền cần, cách nhau bởi dấu cách. Bỏ trống thì nhận mọi quyền của kết nối.
```bash
curl -u "CLIENT_ID:CLIENT_SECRET" \
-d "grant_type=client_credentials" \
-d "scope=https://purl.imsglobal.org/spec/or/v1p2/scope/roster.readonly" \
https://./oauth/token
```
Phản hồi có tiêu đề `Cache-Control: no-store`. Gọi API kèm tiêu đề `Authorization: Bearer `.
## Quyền [#quyền]
| Quyền (scope) | Cho phép đọc |
| --- | --- |
| `https://purl.imsglobal.org/spec/or/v1p2/scope/roster-core.readonly` | Mọi điểm truy cập Danh sách |
| `https://purl.imsglobal.org/spec/or/v1p2/scope/roster.readonly` | Mọi điểm truy cập Danh sách |
| `https://purl.imsglobal.org/spec/or/v1p2/scope/gradebook.readonly` | Mọi điểm truy cập Kết quả |
Khi tạo kết nối, nhà trường chọn "Danh sách" (roster) và/hoặc "Kết quả thực tập" (gradebook).
## Vòng đời của mã [#vòng-đời-của-mã]
- Mã truy cập có hạn **1 giờ**. Hết hạn thì xin mã mới; không có mã làm mới (refresh token).
- Mã chỉ dùng được ở đúng trường đã cấp.
- Nhà trường **thu hồi** kết nối thì mọi mã đã cấp ngừng hoạt động ngay. Muốn dùng tiếp phải tạo kết nối mới.
- Nhà trường có thể đặt **thời hạn** cho kết nối (30, 90, 180, 365 ngày, hoặc không hết hạn) và giới hạn **địa chỉ mạng** được gọi.
- Khoá bí mật chỉ hiện một lần lúc tạo. Mất khoá thì nhà trường bấm **Thay khoá** để cấp khoá mới; khoá cũ còn dùng được 24 giờ.
- Kết nối loại **mã truy cập** (`ttdn_pat_...`) gọi thẳng API OneRoster bằng `Authorization: Bearer`, không cần bước lấy mã. Xem [API của cổng](../api-cong/index.mdx#xác-thực).
Không gửi khoá bí mật qua tin nhắn hay email không mã hoá, và không đưa vào mã nguồn. Lưu ở nơi giữ bí mật của hệ thống (biến môi trường, kho khoá).
## Lỗi khi lấy mã [#lỗi-khi-lấy-mã]
Theo OAuth 2.0 (RFC 6749):
| Mã HTTP | `error` | Khi nào |
| --- | --- | --- |
| 401 | `invalid_client` | Sai mã ứng dụng hoặc khoá bí mật, kết nối đã thu hồi, hoặc kết nối của trường khác |
| 400 | `unsupported_grant_type` | `grant_type` khác `client_credentials` |
| 400 | `invalid_scope` | Xin quyền mà kết nối không có |
| 429 | `slow_down` | Quá 60 lần lấy mã trong 15 phút từ cùng một địa chỉ mạng |
| 503 | `temporarily_unavailable` | Hệ thống tạm thời không phản hồi; thử lại sau ít phút |
# Đăng nhập và tài khoản (/thuc-tap-doanh-nghiep/nguoi-dung/dang-nhap)
Mỗi trường có địa chỉ cổng riêng. Trang **Đăng nhập** có ba thẻ theo vai trò: **Sinh viên**, **Doanh nghiệp**, **Quản trị**. Chọn đúng thẻ của mình; nếu chọn nhầm, trang sẽ báo và có nút chuyển sang thẻ đúng.
## Sinh viên [#sinh-viên]
Tuỳ cách nhà trường cấp tài khoản:
- **Bằng Google của trường:** bấm **Đăng nhập bằng Google của trường** và chọn tài khoản email do trường cấp.
- **Bằng tên đăng nhập và mật khẩu:** nhập tên đăng nhập hoặc mã sinh viên, cùng mật khẩu nhà trường cấp.
Nếu nhà trường cho phép sinh viên tự tạo tài khoản, sinh viên đăng nhập bằng Google lần đầu là có tài khoản, không cần chờ nhập danh sách. Điều kiện: dùng tài khoản Google do trường quản lý, đúng tên miền email nhà trường đã khai, và đang có đợt thực tập mở.
## Doanh nghiệp [#doanh-nghiệp]
Đăng nhập bằng email và mật khẩu đã đăng ký. Doanh nghiệp cần **xác nhận email** trước lần đăng nhập đầu tiên. Chưa có tài khoản thì xem [Đăng ký doanh nghiệp](./doanh-nghiep/dang-ky.mdx).
## Cán bộ nhà trường [#cán-bộ-nhà-trường]
Đăng nhập ở thẻ **Quản trị** bằng email và mật khẩu do đơn vị vận hành cổng cấp.
## Quên mật khẩu [#quên-mật-khẩu]
Bấm **Quên mật khẩu?** ở trang Đăng nhập, nhập email hoặc tên đăng nhập.
Mở email đặt lại mật khẩu. Liên kết dùng được **một lần, trong 1 giờ**. Mỗi tài khoản nhận tối đa 3 email như vậy mỗi giờ.
Nhập mật khẩu mới (từ 8 ký tự) và lưu. Tài khoản bị đăng xuất khỏi mọi thiết bị và nhận email báo đã đổi mật khẩu.
Để bảo vệ tài khoản, trang luôn báo đã gửi, kể cả khi email hay tên đăng nhập không tồn tại. Không nhận được email thì kiểm tra thư rác hoặc liên hệ nhà trường.
Tài khoản đăng nhập bằng Google, hoặc tài khoản sinh viên không có email, không đặt lại mật khẩu qua email được. Hãy liên hệ nhà trường.
## Đổi mật khẩu [#đổi-mật-khẩu]
Khi đã đăng nhập, mở trang thông tin của mình và chọn **Đổi mật khẩu**. Nhập mật khẩu hiện tại và mật khẩu mới (từ 8 ký tự, khác mật khẩu cũ). Tài khoản đăng nhập bằng Google không có mục này.
## Bị tạm chặn đăng nhập [#bị-tạm-chặn-đăng-nhập]
Để chống dò mật khẩu, một tài khoản chỉ được thử đăng nhập 8 lần trong 15 phút, và một mạng chỉ được thử 40 lần trong 15 phút. Vượt quá thì trang báo phải đợi; đợi khoảng 15 phút rồi thử lại.
# Đăng ký tài khoản (/thuc-tap-doanh-nghiep/nguoi-dung/doanh-nghiep/dang-ky)
Mở trang **Đăng ký doanh nghiệp** của trường: liên kết ở chân trang, hoặc **Đăng ký ngay** ở thẻ **Doanh nghiệp** trên trang Đăng nhập.
Nhập email, mật khẩu (ít nhất 8 ký tự) và nhập lại mật khẩu. Trang có thể yêu cầu một bước xác minh chống tự động.
Mở email xác nhận và bấm vào liên kết. Trang báo **Đã xác nhận email** là xong; từ đây đăng nhập được ở thẻ **Doanh nghiệp**.
Không thấy email xác nhận? Kiểm tra thư rác, hoặc yêu cầu gửi lại ở trang xác nhận email.
- Không đăng ký được bằng email thuộc tên miền dành cho sinh viên của trường.
- Mỗi email chỉ gửi yêu cầu đăng ký được 3 lần mỗi giờ.
- Để bảo vệ thông tin, trang trả lời giống nhau dù email đã đăng ký hay chưa.
# Hồ sơ doanh nghiệp (/thuc-tap-doanh-nghiep/nguoi-dung/doanh-nghiep/ho-so)
Sau khi đăng nhập, mở **Hồ sơ doanh nghiệp** và điền:
- **Tên ngắn** và **Tên đầy đủ**.
- **Địa chỉ**, **Email liên hệ**, **Điện thoại**.
- **Giới thiệu chương trình thực tập**: vị trí, công việc, quyền lợi, yêu cầu.
- **Logo** (không bắt buộc).
Thay đổi trên hồ sơ hiện cho sinh viên ngay. Một số thông tin liên hệ chỉ hiện khi sinh viên đã đăng nhập.
## Chính sách thực tập [#chính-sách-thực-tập]
Tải lên tối đa 5 tệp PDF (dung lượng theo quy định của trường, thường tối đa 10 MB) và đặt tên cho từng tệp. Cần ít nhất một tệp để được duyệt; khi đã được duyệt, không xoá được tệp cuối cùng.
# Tổng quan cho doanh nghiệp (/thuc-tap-doanh-nghiep/nguoi-dung/doanh-nghiep)
Mỗi trường có cổng thực tập riêng. Doanh nghiệp muốn nhận sinh viên của trường nào thì đăng ký trên cổng của trường đó.
**Đăng ký tài khoản** và xác nhận email. Xem [Đăng ký tài khoản](./dang-ky.mdx).
**Hoàn thiện hồ sơ doanh nghiệp**, gồm tệp chính sách thực tập. Xem [Hồ sơ doanh nghiệp](./ho-so.mdx).
**Gửi hồ sơ tham gia đợt** để nhà trường duyệt. Xem [Tham gia đợt thực tập](./tham-gia-dot.mdx).
**Xét hồ sơ sinh viên**: nhận hoặc từ chối. Xem [Xét hồ sơ ứng tuyển](./xet-ho-so.mdx).
# Tham gia đợt thực tập (/thuc-tap-doanh-nghiep/nguoi-dung/doanh-nghiep/tham-gia-dot)
Mỗi đợt thực tập, doanh nghiệp gửi hồ sơ tham gia để nhà trường duyệt.
Điền đủ thông tin bắt buộc trong hồ sơ (tên, địa chỉ, email liên hệ, điện thoại, giới thiệu) và tải lên chính sách thực tập.
Bấm **Gửi hồ sơ duyệt**. Nhà trường nhận bản hồ sơ tại thời điểm gửi.
Chờ nhà trường duyệt. Được duyệt, doanh nghiệp hiện trên trang chủ của trường và bắt đầu nhận hồ sơ.
Bị từ chối thì xem **Lý do của nhà trường**, sửa hồ sơ và **Gửi lại hồ sơ** trong cùng đợt. Khi nhà trường mở đợt mới, doanh nghiệp gửi lại hồ sơ cho đợt đó.
## Chỉ tiêu và nhận hồ sơ [#chỉ-tiêu-và-nhận-hồ-sơ]
Nhà trường đặt hai chỉ tiêu cho mỗi doanh nghiệp: số hồ sơ chờ tối đa và số sinh viên tiếp nhận tối đa. Doanh nghiệp thấy trạng thái **Đang nhận đơn** hoặc **Đã đóng nhận đơn**, nhưng không tự mở hay đóng được.
- Đủ số sinh viên tiếp nhận thì hệ thống tự đóng nhận hồ sơ.
- Đủ số hồ sơ chờ thì sinh viên tạm không nộp thêm được cho tới khi doanh nghiệp trả lời bớt.
Khi đã đóng nhận hồ sơ, doanh nghiệp không nhận thêm sinh viên được nữa; các hồ sơ còn chờ chỉ có thể **Từ chối**. Nên trả lời hết hồ sơ trước khi đủ chỉ tiêu hoặc trước khi nhà trường đóng tuyển.
# Xét hồ sơ ứng tuyển (/thuc-tap-doanh-nghiep/nguoi-dung/doanh-nghiep/xet-ho-so)
Mở **Hồ sơ ứng tuyển**. Lọc theo **Chờ duyệt**, **Đã nhận**, **Từ chối**, **Đã đóng**; xem đợt này hoặc mọi đợt; tìm theo tên hoặc mã sinh viên. Mỗi hồ sơ có thông tin sinh viên tại lúc nộp và **Xem CV**.
## Nhận hoặc từ chối [#nhận-hoặc-từ-chối]
Bấm **Nhận sinh viên** hoặc **Từ chối**.
Điền lý do (bắt buộc khi từ chối), nhận xét về CV và ghi chú nếu cần (mỗi ô tối đa 1.000 ký tự), rồi bấm **Tiếp tục**.
Bấm **Chốt nhận** hoặc **Gửi từ chối**. Sinh viên nhận email báo kết quả.
Cả hai quyết định đều **không thay đổi được**. Sinh viên được nhận là có nơi thực tập ngay, không cần xác nhận thêm.
## Khi nhận một sinh viên [#khi-nhận-một-sinh-viên]
Các hồ sơ khác của sinh viên đó tự chuyển sang **Đã đóng**, và các doanh nghiệp liên quan nhận email báo.
## Hồ sơ Đã đóng [#hồ-sơ-đã-đóng]
Là hồ sơ không còn xét được: sinh viên đã có nơi thực tập khác, hoặc bị nhà trường khoá tài khoản. Doanh nghiệp nhận email báo trong cả hai trường hợp.
Hệ thống không gửi email khi có hồ sơ mới; hãy vào trang **Hồ sơ ứng tuyển** thường xuyên.
# Cài đặt trường (/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/cai-dat)
Mở **Cài đặt trường**. Trang chia thành bốn thẻ; sửa ở nhiều thẻ rồi bấm **Lưu thay đổi** một lần (hoặc **Hoàn tác**). Riêng logo và biểu tượng trang được lưu ngay khi tải lên. Thay đổi áp dụng ngay cho mọi người dùng của trường.
## Nhận diện [#nhận-diện]
- **Tên trang** (2 đến 80 ký tự): hiện ở đầu trang và trên thẻ trình duyệt.
- **Màu chính**, **Màu nhấn**, **Màu nền**, có phần xem trước.
- **Logo trường** và **Biểu tượng trang** (hình nhỏ trên thẻ trình duyệt). Chưa có biểu tượng riêng thì dùng logo trường.
## Liên hệ và email [#liên-hệ-và-email]
- **Email liên hệ**: người dùng trả lời email của hệ thống sẽ gửi tới địa chỉ này.
- **Số điện thoại**, **Địa chỉ**: hiện ở cuối mỗi email.
- **Lời nhắn cuối email** (tối đa 300 ký tự), ví dụ giờ làm việc.
- **Gửi email thử** để xem email mẫu mang tên, logo và màu của trường.
## Tài khoản và giới hạn [#tài-khoản-và-giới-hạn]
- **Sinh viên tự tạo tài khoản bằng Google**: xem [Sinh viên tự đăng ký](./sinh-vien-tu-dang-ky.mdx).
- **Số đơn chờ tối đa mỗi sinh viên**: 1 đến 10, mặc định 3.
- **Dung lượng tối đa mỗi tệp (MB)**: 1 đến 10, mặc định 10. Áp dụng cho CV, logo và tệp chính sách thực tập.
## Trang trợ giúp [#trang-trợ-giúp]
Soạn **Hướng dẫn** và **Quy định** hiện ở trang Trợ giúp của trường cho mọi người dùng. Thanh công cụ có định dạng chữ, căn lề, chèn liên kết và ảnh.
# Đơn ứng tuyển (/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/don-ung-tuyen)
Trang **Đơn ứng tuyển** liệt kê mọi hồ sơ của đợt đang mở. Nhà trường chỉ theo dõi; việc nhận hay từ chối do doanh nghiệp quyết định.
- **Lọc** theo trạng thái (đang chờ, được nhận, bị từ chối, đã đóng) và theo doanh nghiệp; tìm kiếm theo tên.
- **Gom theo sinh viên** để xem mỗi sinh viên đã nộp vào đâu.
- **Xem CV** của từng hồ sơ.
- **Xuất CSV** để lưu hoặc báo cáo.
Muốn biết sinh viên nào chưa có nơi thực tập, xem trang **Tổng quan** hoặc lọc hồ sơ theo trạng thái.
# Đợt thực tập (/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/dot-thuc-tap)
Mọi hoạt động diễn ra trong **đợt đang mở**: doanh nghiệp gửi hồ sơ tham gia, sinh viên nộp hồ sơ ứng tuyển. Mỗi lúc chỉ có một đợt mở. Các việc dưới đây làm ở trang **Tổng quan**.
## Mở đợt mới [#mở-đợt-mới]
Bấm **Mở đợt mới**.
Nhập **Tên đợt** (2 đến 80 ký tự), ví dụ "Đợt thực tập học kỳ 2 năm 2026".
Nếu muốn, chọn **Tự đóng lúc**: thời điểm đợt tự đóng, sớm nhất 5 phút sau, muộn nhất 2 năm sau. Để trống thì nhà trường tự đóng.
Mở đợt mới sẽ **đóng ngay đợt đang mở**. Doanh nghiệp phải gửi lại hồ sơ tham gia và nhà trường phải đưa lại danh sách sinh viên cho đợt mới.
## Hẹn giờ đóng [#hẹn-giờ-đóng]
Bấm **Hẹn giờ đóng** (hoặc **Đổi giờ đóng** nếu đã hẹn), chọn thời điểm rồi **Lưu giờ đóng**. Muốn bỏ thì bấm **Bỏ hẹn giờ**. Đến giờ, đợt tự đóng như khi bấm đóng tay.
## Đóng đợt [#đóng-đợt]
Bấm **Đóng đợt này** rồi xác nhận. Từ lúc đó sinh viên không nộp thêm hồ sơ và doanh nghiệp không nhận thêm sinh viên.
- Đợt đã đóng **không mở lại được**. Các đợt cũ vẫn xem được ở mục **Các đợt trước**.
- Hồ sơ còn đang chờ sẽ giữ nguyên trạng thái chờ, không ai xử lý tiếp được nữa. Nên nhắc doanh nghiệp trả lời hết trước khi đóng.
# Duyệt doanh nghiệp (/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/duyet-doanh-nghiep)
Doanh nghiệp muốn xuất hiện trên trang chủ của trường phải gửi hồ sơ tham gia cho đợt đang mở và được nhà trường duyệt.
## Tìm hồ sơ cần duyệt [#tìm-hồ-sơ-cần-duyệt]
Mở trang **Doanh nghiệp**. Các nhóm: **Chờ duyệt**, **Đã duyệt**, **Bị từ chối**, **Chưa gửi**, **Tất cả**. Bấm vào một doanh nghiệp để mở phần chi tiết, gồm:
- **Hồ sơ đã gửi**: bản doanh nghiệp gửi để duyệt, giữ nguyên tại thời điểm gửi.
- **Hồ sơ hiện tại**: thông tin doanh nghiệp đang hiện cho sinh viên.
- **Chính sách thực tập** (tệp PDF).
## Duyệt [#duyệt]
**Đặt chỉ tiêu**: số hồ sơ chờ tối đa và số sinh viên tiếp nhận tối đa của doanh nghiệp trong đợt, rồi bấm **Lưu chỉ tiêu**.
**Kiểm tra điều kiện duyệt**: đủ tên ngắn và tên đầy đủ, đã đặt chỉ tiêu, có ít nhất một tệp chính sách thực tập, đã gửi hồ sơ cho đợt đang mở.
Bấm **Duyệt**. Doanh nghiệp hiện ngay trên trang chủ và bắt đầu nhận hồ sơ.
## Từ chối [#từ-chối]
Bấm **Từ chối** và ghi lý do (không bắt buộc nhưng nên có). Doanh nghiệp thấy lý do, sửa hồ sơ và có thể gửi lại trong cùng đợt.
Nhà trường không gửi email báo kết quả duyệt; doanh nghiệp xem kết quả khi đăng nhập.
# Tổng quan cho nhà trường (/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong)
Cán bộ nhà trường đăng nhập ở thẻ **Quản trị** và dùng các mục trên thanh điều hướng: **Tổng quan**, **Doanh nghiệp**, **Sinh viên**, **Đơn ứng tuyển**, **Nhật ký**, **Cài đặt trường**, **Kết nối hệ thống**.
## Một đợt thực tập, từ đầu đến cuối [#một-đợt-thực-tập-từ-đầu-đến-cuối]
**Chuẩn bị cổng** (làm một lần): tên, màu, logo, thông tin liên hệ, trang trợ giúp. Xem [Cài đặt trường](./cai-dat.mdx).
**Mở đợt thực tập** và hẹn giờ đóng nếu muốn. Xem [Đợt thực tập](./dot-thuc-tap.mdx).
**Đưa sinh viên vào đợt**: nhập từ tệp CSV hoặc thêm từng người. Xem [Nhập danh sách sinh viên](./nhap-sinh-vien.mdx). Phần mềm đào tạo của trường xuất được gói OneRoster thì [tải gói lên](./nhan-goi-oneroster.mdx), hoặc [kết nối](./ket-noi-he-thong.mdx) để phần mềm tự gửi.
**Duyệt doanh nghiệp** gửi hồ sơ tham gia đợt. Xem [Duyệt doanh nghiệp](./duyet-doanh-nghiep.mdx).
**Theo dõi** hồ sơ ứng tuyển và tình hình sinh viên có nơi thực tập. Xem [Đơn ứng tuyển](./don-ung-tuyen.mdx).
**Đóng đợt** khi kết thúc.
Trang **Tổng quan** cho biết đợt đang mở, các số liệu chính và **Việc cần làm** (ví dụ doanh nghiệp đang chờ duyệt, đợt chưa có sinh viên).
# Kết nối hệ thống (/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/ket-noi-he-thong)
Trang **Kết nối hệ thống** dùng khi trường muốn phần mềm quản lý đào tạo (hoặc hệ thống khác) tự **lấy** danh sách và kết quả thực tập, hoặc tự **gửi** sinh viên, đợt thực tập vào cổng, thay vì cán bộ nhập tay.
## Tạo kết nối [#tạo-kết-nối]
Bấm **Tạo kết nối**.
Trả lời **Hệ thống sẽ kết nối là loại nào?**:
- **Phần mềm theo chuẩn OneRoster**: phần mềm của trường có ghi hỗ trợ OneRoster. Cổng cấp mã ứng dụng và khoá bí mật.
- **Hệ thống khác (mã truy cập)**: đơn vị phụ trách tự viết phần kết nối, hoặc chưa chắc. Cổng cấp một mã truy cập.
Chưa rõ thì hỏi đơn vị phụ trách hệ thống; chọn sai cũng không sao, có thể tạo kết nối khác.
Đặt **Tên kết nối** (2 đến 80 ký tự) để biết hệ thống nào đang dùng, ví dụ “Phần mềm đào tạo”.
Xem các việc hệ thống **Được phép** làm. Cổng đã chọn sẵn theo loại kết nối; chỉ thêm những gì thật sự cần. Bấm **Chọn mọi quyền lấy dữ liệu**, **Chọn tất cả** hoặc **Bỏ chọn** để chọn nhanh.
- **Lấy dữ liệu theo chuẩn OneRoster**: **Danh sách**, **Kết quả thực tập**.
- **Lấy dữ liệu từ cổng**: sinh viên, đợt thực tập, sinh viên trong đợt, kết quả thực tập, doanh nghiệp đã duyệt.
- **Gửi dữ liệu vào cổng**: thêm, sửa, khoá sinh viên; tạo, cập nhật đợt; đưa sinh viên vào đợt.
- **Gửi gói OneRoster**: hệ thống tự gửi gói CSV OneRoster.
Chọn **Thời hạn** (30 ngày, 90 ngày, 180 ngày, 1 năm hoặc **Không hết hạn**). Nếu biết địa chỉ mạng của máy chủ sẽ gọi, nhập vào **Chỉ nhận từ các địa chỉ**, mỗi dòng một địa chỉ; để trống thì nhận từ mọi nơi.
Bấm **Tạo kết nối**, sao chép thông tin hiện ra (mã ứng dụng và khoá bí mật, hoặc mã truy cập) và gửi cho đơn vị phụ trách hệ thống qua kênh an toàn. Bấm **Đã lưu lại, đóng**.
Khoá bí mật và mã truy cập **chỉ hiện một lần**. Nếu làm mất, dùng **Thay khoá** để cấp mã mới.
Khung **Địa chỉ kết nối** luôn hiện các địa chỉ cần gửi kèm: địa chỉ API của cổng, địa chỉ lấy mã truy cập, hai địa chỉ OneRoster và tệp mô tả API.
## Theo dõi [#theo-dõi]
Mỗi kết nối hiện loại, quyền, thời hạn, lần dùng gần nhất (kèm địa chỉ mạng) và người tạo. Bấm **Theo dõi** để xem:
- số lần gọi mỗi ngày trong 30 ngày, chia theo **Thành công**, **Lỗi do yêu cầu**, **Lỗi hệ thống**;
- tổng số lần gọi và số bản ghi hệ thống đã gửi lên, theo từng loại thao tác (ví dụ **Xem sinh viên**, **Gửi danh sách sinh viên**);
- các lỗi gần đây kèm lý do dễ hiểu (ví dụ “Mã không đúng, đã hết hạn hoặc đã thu hồi”). Nhiều lỗi lặp lại thì báo đơn vị phụ trách hệ thống.
Cổng chỉ đếm số lượng, không lưu nội dung dữ liệu hệ thống gửi.
## Thay khoá [#thay-khoá]
Bấm **Thay khoá** khi khoá bị lộ, bị mất, hoặc theo định kỳ. Cổng cấp khoá mới ngay (chỉ hiện một lần); khoá cũ vẫn dùng được **24 giờ** để hệ thống kịp đổi, trong thời gian đó kết nối cũ hiện **Đang thay khoá**.
## Thu hồi [#thu-hồi]
Bấm **Thu hồi** để ngừng ngay một kết nối, ví dụ khi thôi dùng phần mềm hoặc nghi bị lộ khoá. Việc này **không hoàn tác được**; muốn dùng tiếp phải tạo kết nối mới. Mỗi trường có tối đa 10 kết nối đang hoạt động.
## Dữ liệu hệ thống gửi vào [#dữ-liệu-hệ-thống-gửi-vào]
Dữ liệu hệ thống gửi vào (sinh viên, đợt, ghi danh, khoá tài khoản) được ghi vào **Nhật ký**, người thực hiện là tên kết nối, và hiện ở mục **Lịch sử nhận dữ liệu** cùng trang. Lượt nào có dòng lỗi thì bấm **Tải danh sách lỗi (CSV)** để xem. Lượt đang chờ hoặc đang xử lý có thể bấm **Huỷ**.
Cổng không xoá sinh viên theo dữ liệu gửi vào, không đổi email, mật khẩu hay cách đăng nhập của tài khoản đã có, và không tự đóng đợt đang mở. Khi hệ thống của trường đã sang kỳ mới, trang hiện nút **Chuyển sang đợt** để nhà trường quyết định; xem [Nhận gói OneRoster](./nhan-goi-oneroster.mdx#chuyển-sang-đợt-mới).
Tài liệu cho đơn vị phụ trách hệ thống: [Tổng quan API](../../lap-trinh/oneroster/index.mdx) và [API của cổng](../../lap-trinh/api-cong/index.mdx).
# Nhận gói OneRoster (/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/nhan-goi-oneroster)
Nhiều phần mềm quản lý đào tạo xuất được **gói CSV OneRoster** (một tệp `.zip`) chứa kỳ học, sinh viên và ghi danh. Tải gói lên cổng thay cho việc [nhập sinh viên](./nhap-sinh-vien.mdx) bằng tệp Excel.
## Tải gói lên [#tải-gói-lên]
Vào **Kết nối hệ thống**, ở mục **Nhận dữ liệu từ gói OneRoster** bấm **Chọn tệp .zip** và chọn gói (tối đa 20 MB).
Bấm **Kiểm tra gói**. Cổng đọc gói và cho xem trước; **chưa thay đổi gì**.
Xem **Kết quả kiểm tra**: số đợt mới, sinh viên mới, sinh viên được cập nhật, sinh viên được đưa vào đợt, tài khoản bị khoá, dòng lỗi. Có dòng lỗi thì bấm **Tải danh sách lỗi (CSV)** để xem lý do.
Bấm **Áp dụng**. Cổng xử lý dần ở nền; trang hiện tiến độ và báo **Đã áp dụng** khi xong.
Kết quả kiểm tra giữ được một thời gian; nếu quá lâu, cổng báo cần tải lại gói.
## Cổng làm gì với gói [#cổng-làm-gì-với-gói]
- Mỗi **kỳ học** trong gói thành một đợt thực tập. Kỳ đã qua thành đợt đã đóng (để lưu lịch sử); kỳ chưa tới thành đợt chưa mở.
- Mọi người có vai trò **sinh viên** trong gói là sinh viên thực tập. Giáo viên, phụ huynh và người khác được bỏ qua.
- Sinh viên đã có trên cổng được cập nhật họ tên và mã; **email, mật khẩu, cách đăng nhập giữ nguyên**. Sinh viên mới có mật khẩu trong gói thì đăng nhập bằng tên đăng nhập, không có thì đăng nhập bằng Google.
- Sinh viên được đưa vào đợt của kỳ mình ghi danh. Gói không có tệp ghi danh thì sinh viên được đưa vào đợt đang mở.
- Sinh viên bị đánh dấu xoá hoặc ngừng hoạt động trong gói thì bị **khoá**.
- Sinh viên **không có trong gói được giữ nguyên**. Cổng không xoá ai theo gói.
## Chuyển sang đợt mới [#chuyển-sang-đợt-mới]
Khi hệ thống của trường đã sang kỳ mới mà cổng vẫn đang mở đợt cũ, cổng **không tự đóng** đợt cũ. Trang báo “Hệ thống của trường đang ở kỳ … còn cổng đang mở đợt …” cùng nút **Chuyển sang đợt**.
Bấm **Áp dụng** trước để cổng tạo đợt mới.
Bấm **Chuyển sang đợt “…”** và đọc kỹ hộp xác nhận.
Bấm xác nhận. Đợt cũ đóng, đợt mới mở.
Đợt đã đóng **không mở lại được**. Doanh nghiệp phải gửi lại hồ sơ tham gia cho đợt mới, như khi [mở đợt mới](./dot-thuc-tap.mdx) bằng tay.
## Lịch sử nhận dữ liệu [#lịch-sử-nhận-dữ-liệu]
Mục **Lịch sử nhận dữ liệu** liệt kê các lần nhận gần đây: gói do cán bộ tải lên (lần **Xem trước** và lần áp dụng) và dữ liệu hệ thống gửi qua API, kèm người gửi, thời điểm, số mới, số cập nhật, số lỗi. Lần đang chờ hoặc đang xử lý có thể bấm **Huỷ**; phần đã xử lý vẫn được giữ.
# Nhập danh sách sinh viên (/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/nhap-sinh-vien)
Chỉ sinh viên có tên trong đợt đang mở mới nộp hồ sơ được. Nhập hay thêm sinh viên cần có đợt đang mở. Các việc dưới đây làm ở trang **Sinh viên**.
## Nhập từ tệp CSV [#nhập-từ-tệp-csv]
Bấm **Nhập từ CSV**, rồi **Tải tệp CSV mẫu** để có tệp đúng định dạng.
Điền danh sách vào tệp (lưu dạng CSV, mã hoá UTF-8; tối đa 20 MB và 50.000 dòng).
Chọn tệp, hoặc dán nội dung vào ô **Hoặc dán nội dung CSV**, rồi bắt đầu nhập. Danh sách dài được gửi thành nhiều đợt nhỏ; có thể **Dừng sau đợt này** và **Tiếp tục phần còn lại**.
Xem kết quả. Dòng lỗi được liệt kê kèm lý do; bấm **Tải danh sách lỗi (CSV)** để sửa rồi nhập lại riêng các dòng đó.
### Các cột [#các-cột]
| Cột | Bắt buộc | Ghi chú |
| --- | --- | --- |
| `studentCode` | Có | Mã sinh viên, tối đa 40 ký tự |
| `email` | Khi đăng nhập bằng Google | Email do trường cấp; cũng là nơi nhận email thông báo |
| `name` | Có | Họ và tên, tối đa 120 ký tự |
| `username` | Không | Tên đăng nhập (3 đến 64 ký tự: chữ không dấu, số, dấu chấm, gạch dưới, gạch ngang); mặc định là mã sinh viên; chỉ dùng khi có `password` |
| `password` | Không | Từ 8 ký tự; để trống thì sinh viên đăng nhập bằng Google |
- **Có dòng tiêu đề** (dòng đầu có chữ `studentCode`): các cột đặt theo thứ tự nào cũng được.
- **Không có dòng tiêu đề:** các cột phải theo đúng thứ tự `studentCode, email, name, username, password`.
```csv title="sinh-vien.csv"
studentCode,email,name,username,password
SV2026001,an.nv@truong.edu.vn,Nguyễn Văn An,,
SV2026002,,Trần Thị Bình,sv2026002,MatKhau@2026
```
Dòng đầu đăng nhập bằng Google; dòng thứ hai đăng nhập bằng tên đăng nhập và mật khẩu.
### Sinh viên đã có tài khoản [#sinh-viên-đã-có-tài-khoản]
Được đưa vào đợt và cập nhật họ tên, mã theo tệp. Cách đăng nhập giữ nguyên: cột `username`, `password` được bỏ qua.
## Thêm một sinh viên [#thêm-một-sinh-viên]
Bấm **Thêm sinh viên**.
Nhập **Mã sinh viên**, **Họ và tên**, **Email**.
Chọn **Cách đăng nhập**: **Google của trường**, hoặc **Tên đăng nhập và mật khẩu** (nhập mật khẩu ban đầu hoặc bấm **Tạo ngẫu nhiên**).
Hệ thống không gửi email cho sinh viên mới thêm; nhà trường tự gửi thông tin đăng nhập cho sinh viên.
# Nhật ký (/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/nhat-ky)
Trang **Nhật ký** có hai thẻ.
## Hoạt động [#hoạt-động]
Ghi lại mọi thao tác quan trọng: ai làm, làm gì, lúc nào. Lọc theo **Từ ngày**, **Đến ngày** (mặc định 30 ngày gần nhất), **Nhóm**, **Người thực hiện**, hoặc tìm kiếm. Bấm một dòng để xem chi tiết. **Xuất CSV** để lưu (tối đa 10.000 dòng mỗi lần).
## Email [#email]
Danh sách email hệ thống đã gửi, với trạng thái **Đang chờ gửi**, **Đã gửi** hoặc **Gửi lỗi**. Xem lại nội dung từng email và bấm **Gửi lại** nếu cần.
- Email gửi lỗi được tự thử lại tối đa 5 lần.
- Email đặt lại mật khẩu và xác nhận email chỉ được giữ 7 ngày.
# Quản lý doanh nghiệp trong đợt (/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/quan-ly-doanh-nghiep)
Với doanh nghiệp đã duyệt, mở phần chi tiết ở trang **Doanh nghiệp** để làm các việc sau.
## Đổi chỉ tiêu [#đổi-chỉ-tiêu]
Sửa số hồ sơ chờ tối đa hoặc số sinh viên tiếp nhận tối đa, rồi **Lưu chỉ tiêu**.
## Đóng tuyển [#đóng-tuyển]
Bấm **Đóng tuyển** khi doanh nghiệp không nhận thêm sinh viên trong đợt. Đủ số sinh viên tiếp nhận thì hệ thống tự đóng tuyển.
Đóng tuyển **không mở lại được** trong đợt. Sau khi đóng, doanh nghiệp chỉ còn **Từ chối** được các hồ sơ đang chờ, không nhận thêm ai.
## Tạm khoá doanh nghiệp [#tạm-khoá-doanh-nghiệp]
Ở mục **Hiển thị trên trang chủ**, bỏ chọn **Cho hiển thị (không khoá)** rồi bấm **Lưu hiển thị**. Doanh nghiệp biến khỏi trang chủ, hiện nhãn **Đang khoá** và không nhận thêm hồ sơ. Chọn lại để mở khoá.
## Nhà tài trợ [#nhà-tài-trợ]
Bật **Nhà tài trợ** để doanh nghiệp có trang giới thiệu riêng (thay vì hộp thoại chi tiết) và hiện ở nhóm **Nhà tài trợ** trên trang chủ.
# Quản lý sinh viên (/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/quan-ly-sinh-vien)
Trang **Sinh viên** liệt kê sinh viên của trường. Cột **Quyền** cho biết tình trạng:
- **Được ứng tuyển**: có tên trong đợt đang mở.
- **Chỉ xem**: chưa có tên trong đợt đang mở, vào được cổng nhưng không nộp hồ sơ được.
- **Bị khoá**: không đăng nhập được.
Danh sách xuất được ra tệp bằng **Xuất CSV**.
## Sửa họ tên, mã sinh viên [#sửa-họ-tên-mã-sinh-viên]
Chọn **Sửa họ tên, mã sinh viên** ở dòng của sinh viên. Lưu xong, mã sinh viên được xem là đã xác nhận (kể cả mã sinh viên tự khai).
## Sửa đăng nhập [#sửa-đăng-nhập]
Chọn **Sửa đăng nhập** để đổi giữa Google và mật khẩu, đổi email, tên đăng nhập hoặc đặt mật khẩu mới. Sinh viên bị đăng xuất khỏi mọi thiết bị và nhận email báo.
## Khoá và mở khoá [#khoá-và-mở-khoá]
Chọn **Khoá** (hoặc chọn nhiều sinh viên, tối đa 100 một lúc). Khi khoá:
- Sinh viên bị đăng xuất và không đăng nhập được.
- Các hồ sơ đang chờ bị rút; sinh viên và các doanh nghiệp liên quan nhận email báo.
- Nơi thực tập đã được nhận vẫn giữ.
**Mở khoá** để sinh viên dùng lại tài khoản; sinh viên nhận email báo. Hồ sơ đã bị rút không tự quay lại.
# Sinh viên tự đăng ký (/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/sinh-vien-tu-dang-ky)
Thay vì nhập danh sách, nhà trường có thể cho sinh viên tự tạo tài khoản khi đăng nhập bằng tài khoản Google do trường cấp.
## Bật tính năng [#bật-tính-năng]
Mở **Cài đặt trường**, thẻ **Tài khoản và giới hạn**.
Ở **Tên miền email của trường**, thêm tên miền email sinh viên, ví dụ `@sv.truong.edu.vn` (tối đa 5; không nhận email công cộng như gmail.com).
Chọn cách xử lý **Khi có sinh viên mới đăng nhập**, rồi **Lưu thay đổi**.
| Lựa chọn | Điều xảy ra |
| --- | --- |
| **Không tạo tài khoản** | Chỉ sinh viên đã có trong danh sách mới đăng nhập được |
| **Tạo tài khoản, chờ trường duyệt** | Sinh viên vào được cổng nhưng chỉ nộp hồ sơ sau khi nhà trường duyệt |
| **Tạo tài khoản và cho ứng tuyển ngay** | Sinh viên được đưa vào đợt đang mở ngay lần đăng nhập đầu; ở các đợt sau cũng tự vào khi đăng nhập |
Điều kiện: nền tảng đã bật đăng nhập Google, sinh viên dùng tài khoản Google do trường quản lý, và đang có đợt mở.
## Duyệt sinh viên tự đăng ký [#duyệt-sinh-viên-tự-đăng-ký]
Ở trang **Sinh viên**, bộ lọc **Chờ duyệt** liệt kê sinh viên đã tự tạo tài khoản nhưng chưa có tên trong đợt. Bấm **Duyệt** (làm được cho nhiều sinh viên một lúc) để đưa vào đợt; sinh viên nhận email báo và mã sinh viên tự khai được xác nhận.
## Rà soát mã sinh viên [#rà-soát-mã-sinh-viên]
Sinh viên tự đăng ký tự khai mã sinh viên. Hai bộ lọc giúp kiểm tra:
- **Chưa có MSSV**: sinh viên chưa khai mã, nên chưa nộp hồ sơ được.
- **MSSV tự khai**: mã do sinh viên khai, chưa được nhà trường xác nhận.
Sửa mã bằng **Sửa họ tên, mã sinh viên** để xác nhận.
# Chuẩn bị hồ sơ (/thuc-tap-doanh-nghiep/nguoi-dung/sinh-vien/chuan-bi)
Mở **Thông tin cá nhân** và kiểm tra các mục sau.
## Bắt buộc để nộp hồ sơ [#bắt-buộc-để-nộp-hồ-sơ]
- **Số điện thoại:** 10 hoặc 11 chữ số, không trùng với sinh viên khác của trường.
- **Mã sinh viên:** thường do nhà trường nhập sẵn. Nếu bạn tự tạo tài khoản bằng Google, bạn tự khai mã ở đây (3 đến 40 ký tự). Mã khoá lại khi nhà trường xác nhận hoặc khi bạn nộp hồ sơ đầu tiên, nên hãy khai đúng.
- **CV:** một tệp PDF, tải lên lúc nộp hồ sơ (dung lượng tối đa theo quy định của trường, thường là 10 MB).
## Nên có [#nên-có]
- **Email nhận thông tin:** gửi kèm hồ sơ để doanh nghiệp liên hệ với bạn.
- **Ngành học**, **Lớp**, **Kỹ năng**, **Giới thiệu bản thân**, liên kết tới sản phẩm hoặc trang cá nhân.
Doanh nghiệp xem các thông tin này cùng CV khi xét hồ sơ. Họ tên, email đăng nhập và tên đăng nhập do nhà trường quản lý, bạn không tự sửa được.
## Tình trạng của bạn [#tình-trạng-của-bạn]
Trang **Thông tin cá nhân** cho biết số hồ sơ **Đang chờ**, **Được nhận**, **Tổng số đơn**, và tình trạng: **Đã có nơi thực tập**, **Chưa có nơi thực tập**, hoặc **Chưa có tên trong đợt đang mở**.
# Tổng quan cho sinh viên (/thuc-tap-doanh-nghiep/nguoi-dung/sinh-vien)
**Đăng nhập** bằng tài khoản nhà trường cấp. Xem [Đăng nhập và tài khoản](../dang-nhap.mdx).
**Chuẩn bị hồ sơ**: số điện thoại, mã sinh viên, CV. Xem [Chuẩn bị hồ sơ](./chuan-bi.mdx).
**Tìm doanh nghiệp** phù hợp. Xem [Tìm doanh nghiệp](./tim-doanh-nghiep.mdx).
**Nộp hồ sơ** và chờ doanh nghiệp trả lời. Xem [Nộp hồ sơ](./nop-ho-so.mdx) và [Kết quả](./ket-qua.mdx).
Chỉ sinh viên có tên trong đợt thực tập đang mở mới nộp hồ sơ được. Trang **Thông tin cá nhân** báo **Chưa có tên trong đợt đang mở** thì hãy liên hệ nhà trường.
# Kết quả (/thuc-tap-doanh-nghiep/nguoi-dung/sinh-vien/ket-qua)
## Được nhận [#được-nhận]
Bạn có nơi thực tập ngay, không cần xác nhận thêm. Các hồ sơ khác đang chờ của bạn tự chuyển sang **Đã đóng**, và bạn không nộp thêm hồ sơ được.
## Bị từ chối [#bị-từ-chối]
Bạn nhận email kèm lý do. Hồ sơ đó không còn tính vào giới hạn, bạn có thể nộp vào doanh nghiệp khác.
## Email bạn nhận [#email-bạn-nhận]
Email gửi tới **email của tài khoản** (email dùng để đăng nhập hoặc email nhà trường đã nhập), khi:
- Doanh nghiệp nhận hoặc từ chối hồ sơ.
- Nhà trường duyệt tài khoản tự đăng ký, khoá hoặc mở khoá tài khoản, hay đổi cách đăng nhập.
- Bạn đặt lại mật khẩu.
Tài khoản không có email sẽ không nhận được email; hãy theo dõi ở trang **Thông tin cá nhân**.
# Nộp hồ sơ (/thuc-tap-doanh-nghiep/nguoi-dung/sinh-vien/nop-ho-so)
Ở doanh nghiệp muốn ứng tuyển, bấm **Nộp hồ sơ**.
Chọn CV: **Dùng lại CV gần nhất** hoặc **Tải CV mới** (tệp PDF).
Bấm **Tiếp tục**, kiểm tra lại, rồi **Xác nhận nộp**.
Hồ sơ đã nộp **không sửa và không rút lại được**. Kiểm tra kỹ CV trước khi xác nhận.
## Nộp được bao nhiêu hồ sơ [#nộp-được-bao-nhiêu-hồ-sơ]
Mỗi lúc bạn chỉ có một số hồ sơ **đang chờ** theo giới hạn của nhà trường (thường là 3). Hồ sơ được trả lời (nhận hoặc từ chối) thì không còn tính, bạn nộp tiếp được. Mỗi doanh nghiệp chỉ nộp một lần trong một đợt.
## Khi nào không nộp được [#khi-nào-không-nộp-được]
- Chưa có CV, số điện thoại hoặc mã sinh viên.
- Không có đợt đang mở, hoặc bạn chưa có tên trong đợt.
- Bạn đã có nơi thực tập.
- Số hồ sơ đang chờ của bạn đã đạt giới hạn.
- Bạn đã nộp vào doanh nghiệp này trong đợt.
- Doanh nghiệp đã đủ số hồ sơ chờ hoặc đủ số sinh viên tiếp nhận, đã đóng nhận hồ sơ, hoặc đang bị nhà trường tạm ẩn.
Hệ thống không gửi email khi bạn vừa nộp hồ sơ; theo dõi ở trang **Thông tin cá nhân**.
# Tìm doanh nghiệp (/thuc-tap-doanh-nghiep/nguoi-dung/sinh-vien/tim-doanh-nghiep)
Trang **Khám phá** (trang chủ của cổng) liệt kê các doanh nghiệp tham gia đợt đang mở.
- **Tìm** theo tên doanh nghiệp.
- **Lọc**: **Tất cả**, **Đang tuyển**, **Nhà tài trợ**, **Đã đủ**.
- **Sắp xếp**: ngẫu nhiên, theo tên, hoặc còn nhiều chỉ tiêu nhất.
- **Đổi cách xem**: lưới thẻ, danh sách, hoặc tường logo.
Mở một doanh nghiệp để xem giới thiệu chương trình thực tập, số chỉ tiêu còn lại và chính sách thực tập. Địa chỉ, thông tin liên hệ và tệp chính sách chỉ hiện khi đã đăng nhập.
Doanh nghiệp **Đã đủ** không nhận thêm hồ sơ; hãy chọn nơi khác.
# Company internships (/en/thuc-tap-doanh-nghiep)
The portal helps a school run internship seasons: companies register to take part, the school approves them, students find companies and apply, and companies accept or reject. Each school has its own portal, with its own name, colours and rules.
## How a season runs [#how-a-season-runs]
**The school opens a season** and adds its students to it.
**Companies submit their profile for the season**; the school reviews and approves.
**Students browse companies** on the school's home page and apply with a CV.
**Companies accept or reject.** An accepted student is placed at once.
**The school follows progress** and closes the season at the end.
## Guides [#guides]
The user guides are in Vietnamese for now.
## For system integrators [#for-system-integrators]
The school's student information system can read rosters and internship results through a OneRoster 1.2 API. See the [API overview](./lap-trinh/oneroster/index.mdx).
# Reading data (/en/thuc-tap-doanh-nghiep/lap-trinh/api-cong/doc-du-lieu)
Paths below are relative to `https://./integration/v1`.
| Method | Path | Scope |
| --- | --- | --- |
| `GET` | `/seasons` | `seasons:read` |
| `GET` | `/seasons/{id}` | `seasons:read` |
| `GET` | `/students` | `students:read` |
| `GET` | `/students/{studentCode}` | `students:read` |
| `GET` | `/seasons/{id}/enrollments` | `enrollments:read` |
| `GET` | `/seasons/{id}/results` | `results:read` |
| `GET` | `/seasons/{id}/companies` | `companies:read` |
## Seasons [#seasons]
`GET /seasons` returns every season of the school (up to 500, oldest first). Filter by term id: `?externalId=HK1-2026`.
```json
{
"data": [
{
"id": "k3v9x2m1q8w7e6r5t4y3u2i1",
"externalId": "HK1-2026",
"name": "Đợt thực tập học kỳ 1 năm học 2026-2027",
"status": "open",
"openedAt": "2026-09-01T01:00:00.000Z",
"closesAt": "2027-01-15T16:59:59.000Z",
"closedAt": null,
"updatedAt": "2026-11-02T03:12:44.000Z"
}
]
}
```
`status`: `open`, `upcoming` (not opened yet) or `closed`. A school has at most one open season.
## Students [#students]
`GET /students` returns the school's students in pages (see [Paging](./index.mdx#paging)).
| Parameter | Meaning |
| --- | --- |
| `limit`, `cursor` | Paging |
| `updatedSince` | Only students changed since this instant (ISO 8601) |
| `status` | `active` or `blocked` |
| `season` | A season `id`: only students in that season |
```json
{
"data": [
{
"id": "p0o9i8u7y6t5r4e3w2q1a2s3",
"studentCode": "SV2026001",
"externalId": "U-1001",
"name": "Nguyễn Văn An",
"email": "an.nv@truong.edu.vn",
"signIn": "google",
"loginName": null,
"status": "active",
"createdAt": "2026-09-02T02:00:00.000Z",
"updatedAt": "2026-11-02T03:12:44.000Z"
}
],
"next": "eyJhZnRlciI6MTIzfQ"
}
```
- `signIn`: `google` (Google account matching `email`) or `password` (`loginName` and password).
- `email` is `null` for a login-name account without a real email.
`GET /students/{studentCode}` returns one student in `data`, or `404`.
## Season enrollments [#season-enrollments]
`GET /seasons/{id}/enrollments`, in pages:
```json
{
"data": [
{
"id": "z1x2c3v4b5n6m7a8s9d0f1g2",
"externalId": null,
"season": "k3v9x2m1q8w7e6r5t4y3u2i1",
"student": { "id": "p0o9i8u7y6t5r4e3w2q1a2s3", "studentCode": "SV2026001", "externalId": "U-1001", "name": "Nguyễn Văn An" },
"placed": false,
"admittedAt": "2026-09-02T02:00:00.000Z"
}
],
"next": null
}
```
`placed`: whether the student has an internship in this season.
## Internship results [#internship-results]
`GET /seasons/{id}/results` returns, for each student in the season, the confirmed placement:
```json
{
"data": [
{
"student": { "id": "p0o9i8u7y6t5r4e3w2q1a2s3", "studentCode": "SV2026001", "externalId": "U-1001", "name": "Nguyễn Văn An" },
"season": "k3v9x2m1q8w7e6r5t4y3u2i1",
"placed": true,
"company": { "id": "c8d7e6f5g4h3j2k1l0m9n8b7", "name": "Công ty Cổ phần Ví dụ" },
"acceptedAt": "2026-10-05T08:30:00.000Z",
"pendingApplications": 0
}
],
"next": null
}
```
- `placed: false` and `company: null`: no placement yet.
- `pendingApplications`: applications waiting for the company or for the student's answer.
## Approved companies [#approved-companies]
`GET /seasons/{id}/companies` returns the companies the school approved for the season (no paging):
```json
{
"data": [
{
"id": "c8d7e6f5g4h3j2k1l0m9n8b7",
"name": "Công ty Cổ phần Ví dụ",
"shortName": "Ví dụ",
"sponsor": false,
"hiringOpen": true,
"maximumWaitingApplications": 30,
"maximumAcceptedStudents": 10
}
]
}
```
`hiringOpen`: the company still accepts applications. The two maximums are `null` when unlimited.
# Limits and errors (/en/thuc-tap-doanh-nghiep/lap-trinh/api-cong/gioi-han-va-loi)
## Limits [#limits]
Limits protect each school's portal from floods (OWASP API Security, API4: unrestricted resource consumption). The numbers below are per-connection defaults; ask the school if you need others.
| Limit | Default |
| --- | --- |
| Read calls | 600 a minute |
| Write calls | 60 a minute |
| Concurrent calls | 4 |
| Records sent | 2,000 a minute, 50,000 a day |
| Records a school receives (all connections together) | 5,000 a minute |
| Students per batch | 500 |
| JSON body | 5 MB |
| OneRoster CSV package | 20 MB, 50,000 rows per file |
| Jobs waiting or running per school | 4 |
Jobs are paced by the record limits, so a large package can take a few minutes.
## Rate-limit headers [#rate-limit-headers]
Every answer carries headers from the IETF draft *RateLimit header fields for HTTP*:
```http
RateLimit-Policy: "read";q=600;w=60
RateLimit: "read";r=596;t=42
```
- `q`: calls allowed per `w` seconds. `r`: calls left. `t`: seconds until the window resets.
- Writes add a `"records"` policy for records per minute.
Going over returns `429 Too Many Requests` with `Retry-After` (seconds). Wait that long, then retry; send writes with an `Idempotency-Key` so retries never create duplicates.
## Errors [#errors]
Errors are Problem Details (RFC 9457), `application/problem+json`:
```json
{
"type": "urn:ttdn:problem:invalid-request",
"title": "The request is not valid",
"status": 422,
"detail": "Some students are not valid; nothing was queued.",
"instance": "/integration/v1/students/batch",
"errors": [{ "index": 3, "ref": "SV2026004", "message": "studentCode and name are required." }]
}
```
Act on `type`; `title` and `detail` are for people.
| HTTP | `type` (`urn:ttdn:problem:...`) | When |
| --- | --- | --- |
| 401 | `unauthorized` | Missing, wrong, expired or revoked token, or a token of another school |
| 403 | `insufficient-scope` | The token lacks the scope for this endpoint |
| 403 | `ip-not-allowed` | Called from an address outside the school's allowlist |
| 404 | `not-found` | No such season, student or job |
| 409 | `conflict`, `code-taken`, `login-name-taken`, `declared-code` | The data points at another account (email, student code or login name already belongs to someone else) |
| 409 | `season-closed` | Admitting students to a closed season |
| 413 | `too-large` | Body too large, or more than 500 students |
| 422 | `invalid-request` | Invalid data; `errors` lists each row |
| 422 | `invalid-package` | The OneRoster CSV package cannot be read |
| 422 | `idempotency-key-reused`, `invalid-idempotency-key` | The `Idempotency-Key` was used for a different body, or is longer than 255 characters |
| 429 | `rate-limited` | Over a limit; wait for `Retry-After` |
| 500 | `internal-error` | Portal error; retry later and tell the school if it persists |
| 503 | `busy` | The school already has 4 jobs waiting; wait for `Retry-After` |
## What the school sees [#what-the-school-sees]
Under **Kết nối hệ thống**, each connection's **Theo dõi** view shows the school calls per day for 30 days (success, request errors, server errors), records sent and recent errors. The portal counts only; it **does not store request contents**. Daily numbers are kept 13 months, recent errors 30 days.
# OneRoster CSV packages (/en/thuc-tap-doanh-nghiep/lap-trinh/api-cong/goi-oneroster)
Many student information systems export **OneRoster CSV packages** (one `.zip` file). The portal takes them in two ways:
- School staff upload the package under **Kết nối hệ thống** (always previewed before it is applied).
- The school's system sends it over the API: `POST /integration/v1/imports/oneroster`, scope `imports:write`.
## Sending over the API [#sending-over-the-api]
The body is the zip file itself, `Content-Type: application/zip`, up to **20 MB**.
```bash
curl -X POST -H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/zip" \
-H "Idempotency-Key: 2026-11-13-oneroster" \
--data-binary @oneroster.zip \
"https://./integration/v1/imports/oneroster?dryRun=true"
```
The answer is `202` with a job; follow it at `/jobs/{id}` as in [Sending data](./gui-du-lieu.mdx#following-a-job). With `?dryRun=true`, `counts` shows what would change and nothing is saved; drop `dryRun` to apply. `warnings.package` holds the package version, each file's mode, how many non-student users were skipped, and notes.
An unreadable package returns `422` with `type` `urn:ttdn:problem:invalid-package` and the reason in `detail`.
## What the package needs [#what-the-package-needs]
| File | Required | Used for |
| --- | --- | --- |
| `manifest.csv` | yes | `oneroster.version` (`1.2`; `1.1` is also read) and `file.` = `bulk`, `delta` or `absent` for each file |
| `users.csv` | yes | Users; only those with the student role are taken |
| `roles.csv` | for 1.2 | Users' roles (1.1 uses the `role` column of `users.csv`) |
| `academicSessions.csv` | no | Terms (`term`, `semester`) become seasons |
| `classes.csv` | no | Classes, to know each enrollment's term |
| `enrollments.csv` | no | Students' class enrollments |
Other files (`orgs.csv`, `courses.csv`, `demographics.csv`...) are ignored. Each file has a header row and up to 50,000 rows.
## How the portal reads it [#how-the-portal-reads-it]
- **Seasons:** each term (`term` or `semester`) used by the package's classes becomes a season, placed by its dates as when [sending seasons](./gui-du-lieu.mdx#seasons). Without classes, every term is used.
- **Students:** everyone with the `student` role. The student code is taken from `identifier`, then `userIds` as `{studentCode:SV2026001}`, then `username`. The full name is built in Vietnamese order: `familyName middleName givenName`. `sourcedId` becomes `externalId`.
- **Sign-in:** a row with `password` signs in with a login name (`username`, or the student code) and that password; without one, with Google using `email`.
- **Enrollments:** a student enrolled in a class is admitted to the season of that class's term. Without `enrollments.csv`, students are admitted to the open season.
- **Blocking:** `status = tobedeleted` or `enabledUser = false` blocks the account.
- **No deletion:** students missing from a `bulk` file are left as they are. Deleted terms and enrollments are noted, never applied.
- Users with other roles (teachers, guardians...) are skipped and counted in `ignoredUsers`.
When a term is in progress while the school has another season open, the portal creates the new season as upcoming and reports `switchSeason` in `warnings`. School staff confirm the switch under **Kết nối hệ thống**.
# Sending data (/en/thuc-tap-doanh-nghiep/lap-trinh/api-cong/gui-du-lieu)
Paths are relative to `https://./integration/v1`. Bodies are JSON (`Content-Type: application/json`), up to 5 MB.
| Method | Path | Scope | Handling |
| --- | --- | --- | --- |
| `PUT` | `/seasons/external/{externalId}` | `seasons:write` | Immediate |
| `PUT` | `/students/{studentCode}` | `students:write` | Immediate |
| `POST` | `/students/batch` | `students:write` | Job (`202`) |
| `POST` | `/students/{studentCode}/block` | `students:write` | Immediate |
| `POST` | `/students/{studentCode}/unblock` | `students:write` | Immediate |
| `POST` | `/seasons/{id}/enrollments` | `enrollments:write` | Job (`202`) |
| `GET` | `/jobs/{id}` | any write scope | |
| `GET` | `/jobs/{id}/failures` | any write scope | |
Batch writes are always **queued** and processed in the background, so one large send cannot slow the portal down. Every write accepts `?dryRun=true`.
## Rules [#rules]
- **Nothing is deleted.** The API never deletes students or seasons. Students who leave are **blocked**; students missing from a send are left as they are.
- **Existing accounts keep how they sign in.** Updates change the name, student code and `externalId`; an existing account's email, password and sign-in method stay.
- **Every student sent is an internship student** of the school.
- Every change is written to the school's **Nhật ký** (log), with the connection's name as the actor.
## Seasons [#seasons]
`PUT /seasons/external/{externalId}` creates or updates a season from a term in the school's system. `externalId` is the term's id (up to 128 characters).
```bash
curl -X PUT -H "Authorization: Bearer ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Học kỳ 2 năm học 2026-2027","startDate":"2027-02-10","endDate":"2027-06-30"}' \
https://./integration/v1/seasons/external/HK2-2026
```
| Field | Required | Notes |
| --- | --- | --- |
| `name` | yes | 2 to 80 characters |
| `startDate`, `endDate` | no | `YYYY-MM-DD`; `endDate` not before `startDate` |
The portal places a new season by the term's dates:
| Term | New season |
| --- | --- |
| Already ended | **Closed** (kept for history) |
| In progress, no season open | **Open**, closing at the end of `endDate` |
| In progress, another season open | **Upcoming**; the answer has `switchNeeded` |
| Not started | **Upcoming** |
The portal **never closes** the open season by itself: with `switchNeeded`, school staff confirm the switch under **Kết nối hệ thống**. Sending the same `externalId` again updates the name and closing date. An upcoming season opens when you send it again while its term is in progress and no other season is open (the portal does not open seasons on a schedule).
```json
{
"result": "created",
"data": { "id": "k3v9x2m1q8w7e6r5t4y3u2i1", "externalId": "HK2-2026", "name": "Học kỳ 2 năm học 2026-2027", "status": "upcoming", "openedAt": null, "closesAt": "2027-06-30T16:59:59.000Z", "closedAt": null, "updatedAt": "2026-11-13T02:00:00.000Z" },
"switchNeeded": { "from": "Đợt thực tập học kỳ 1 năm học 2026-2027", "to": "Học kỳ 2 năm học 2026-2027" }
}
```
`result`: `created`, `opened`, `updated`, `unchanged`. `201` when created, `200` when updated.
## One student [#one-student]
`PUT /students/{studentCode}` creates or updates one student (no season change).
| Field | Required | Notes |
| --- | --- | --- |
| `name` | yes | Full name, up to 120 characters |
| `email` | without `password` | The student's Google account; up to 254 characters |
| `password` | no | 8 to 128 characters. With a password the student signs in with a login name |
| `username` | no | Login name (3 to 64 letters, digits, `.`, `-`, `_`), only with `password`. Defaults to the student code |
| `externalId` | no | The student's id in the school's system, up to 128 characters |
```bash
curl -X PUT -H "Authorization: Bearer ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Nguyễn Văn An","email":"an.nv@truong.edu.vn","externalId":"U-1001"}' \
https://./integration/v1/students/SV2026001
```
```json
{ "result": "created", "data": { "id": "p0o9i8u7y6t5r4e3w2q1a2s3", "studentCode": "SV2026001", "externalId": "U-1001", "name": "Nguyễn Văn An", "email": "an.nv@truong.edu.vn", "signIn": "google", "loginName": null, "status": "active", "createdAt": "2026-11-13T02:00:00.000Z", "updatedAt": "2026-11-13T02:00:00.000Z" } }
```
`result`: `created` (`201`), `updated`, `unchanged`. The portal finds the student by `externalId`, then student code, then email. It returns `409` when the data points to two different accounts, for example an email that belongs to a student with another code, or a student code already used as someone's login name.
## Many students [#many-students]
`POST /students/batch` takes up to **500** students per request, with the fields above plus `studentCode`. Add `season` (a season `id`) to also admit them to that season.
```bash
curl -X POST -H "Authorization: Bearer ACCESS_TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a0e-6d55-4a8e-9a57-0f3b8e7d2c11" \
-d '{"season":"k3v9x2m1q8w7e6r5t4y3u2i1","students":[{"studentCode":"SV2026001","name":"Nguyễn Văn An","email":"an.nv@truong.edu.vn"},{"studentCode":"SV2026002","name":"Trần Thị Bình","password":"MatKhau@2026"}]}' \
https://./integration/v1/students/batch
```
The whole batch is checked first: any invalid row (missing field, duplicate code in the batch) returns `422` with an `errors` list and **nothing is queued**. Otherwise the answer is `202 Accepted` with a `Location` header pointing at the job:
```json
{ "job": { "id": "at8w3w2oxfpggp9ng0w5a3de", "kind": "students", "status": "queued", "dryRun": false, "total": 2, "processed": 0, "counts": {}, "warnings": {}, "failureCount": 0, "createdAt": "2026-11-13T02:00:00.000Z", "startedAt": null, "finishedAt": null, "by": "Kết nối “Phần mềm đào tạo”" } }
```
## Admitting students to a season [#admitting-students-to-a-season]
`POST /seasons/{id}/enrollments` takes up to 500 existing students, each `{ "studentCode": "..." }` or `{ "externalId": "..." }`. A closed season returns `409`. The answer is a job (`202`), as above.
## Blocking and unblocking [#blocking-and-unblocking]
`POST /students/{studentCode}/block` blocks the account, exactly as when the school blocks it on screen: the student can no longer sign in, applications waiting for a decision or an answer are withdrawn and the companies are told. `POST /students/{studentCode}/unblock` reopens it. No body; the answer is the student after the change.
## Following a job [#following-a-job]
`GET /jobs/{id}` returns the job's state (only jobs sent by the same connection):
| `status` | Meaning |
| --- | --- |
| `queued` | Waiting |
| `running` | In progress; `processed` / `total` shows how far |
| `succeeded` | Done (some rows may have failed, see `failureCount`) |
| `failed` | Could not run |
| `cancelled` | Cancelled by the school |
`counts` tallies outcomes, such as `students.created`, `students.updated`, `students.blocked`, `enrollments.enrolled`, `seasons.created`, `failed`. Poll every 2 to 5 seconds.
`GET /jobs/{id}/failures` returns failed rows as CSV (`ref,code,message`). Failed rows are kept for 30 days.
# Portal API overview (/en/thuc-tap-doanh-nghiep/lap-trinh/api-cong)
Not every school system supports OneRoster. The **portal API (v1)** is a plain JSON REST API that lets the school's software **read** and **send** internship data: students, seasons, season enrollments, account blocks; and read internship results and approved companies. It also accepts **OneRoster CSV packages** from software that can export them but has no OneRoster API.
Systems that support OneRoster 1.2 should read through the [OneRoster API](../oneroster/index.mdx). Both APIs share connections, scopes and limits.
## At a glance [#at-a-glance]
- **Base URL:** `https://./integration/v1`. The school is identified by the domain; a token only works at the school that issued it.
- **Format:** JSON, UTF-8. Instants in ISO 8601 (UTC); dates (`startDate`, `endDate`) as `YYYY-MM-DD` in Vietnam time.
- **Errors:** Problem Details (RFC 9457), `application/problem+json`. See [Limits and errors](./gioi-han-va-loi.mdx).
- **Machine-readable description:** OpenAPI 3.1 at `/integration/v1/openapi.json`.
## Authentication [#authentication]
Every call sends `Authorization: Bearer `. The school creates a connection under **Kết nối hệ thống** and chooses one of two kinds:
| Connection kind | How it works | Suits |
| --- | --- | --- |
| **Access token** | One `ttdn_pat_...` token sent with every call. Optional expiry (30, 90, 180, 365 days) or none. | Simple systems, sync scripts |
| **Client id and secret** | Get a one-hour token at `/oauth/token` with OAuth 2.0 Client Credentials, as for [OneRoster](../oneroster/xac-thuc.mdx). | Systems used to OAuth 2.0 |
```bash
curl -H "Authorization: Bearer ACCESS_TOKEN" \
"https://./integration/v1/seasons"
```
- **Rotation:** the school presses **Thay khoá** to issue a new token (or secret); the old one keeps working for **24 hours**.
- **Revocation:** stops every token of the connection at once; it cannot be undone.
- **Address allowlist:** if the school lists network addresses (IPv4/IPv6, CIDR such as `203.0.113.0/24`), calls from other addresses get `403`.
Tokens and secrets are shown only once. Keep them in environment variables or a secret store, never in source code or unencrypted messages.
## Scopes [#scopes]
| Scope | Allows |
| --- | --- |
| `students:read` | Read students |
| `students:write` | Create, update, block, unblock students |
| `seasons:read` | Read seasons |
| `seasons:write` | Create and update seasons from the school's terms |
| `enrollments:read` | Read the students of a season |
| `enrollments:write` | Admit students to a season |
| `results:read` | Read internship results |
| `companies:read` | Read the companies approved for a season |
| `imports:write` | Send OneRoster CSV packages |
Checking jobs (`/jobs/...`) needs any write scope: `students:write`, `enrollments:write` or `imports:write`. A missing scope returns `403` naming the scope needed.
## Conventions [#conventions]
### Paging [#paging]
Long lists (`/students`, `/seasons/{id}/enrollments`, `/seasons/{id}/results`) come in pages:
- `limit`: 1 to 500, default 100.
- The body has `next`: the cursor of the next page, or `null` at the end. Send it back as `?cursor=` with the same parameters.
- The `Link: <...>; rel="next"` header (RFC 8288) holds the next page's URL.
### Syncing changes [#syncing-changes]
`GET /students?updatedSince=2026-11-01T00:00:00Z` returns only students changed since then. Store the time each sync starts and use it next time.
### Dry runs [#dry-runs]
Every write accepts `?dryRun=true`: the portal checks and answers as if for real but **saves nothing**.
### Safe retries [#safe-retries]
Batch writes (`POST`) accept an `Idempotency-Key` header (1 to 255 characters; a UUID is best). Resending with the same key within **24 hours** returns the first answer (with `Idempotent-Replayed: true`) and creates no new job. The same key with a different body is refused (`422`).
### Identifiers [#identifiers]
- Students are identified by **student code** (`studentCode`, case-insensitive).
- `externalId` (optional, up to 128 characters) is the record's id in the school's system. Once sent, the portal matches students by it first, then by student code, then by email.
- Seasons have a portal `id` and an `externalId`: the term's id in the school's system.
## Endpoint groups [#endpoint-groups]
| Group | Page |
| --- | --- |
| Read seasons, students, enrollments, results, companies | [Reading data](./doc-du-lieu.mdx) |
| Send seasons, students, enrollments, blocks; follow jobs | [Sending data](./gui-du-lieu.mdx) |
| Send OneRoster CSV packages | [OneRoster CSV packages](./goi-oneroster.mdx) |
# Roster data (/en/thuc-tap-doanh-nghiep/lap-trinh/oneroster/du-lieu)
Base path: `/ims/oneroster/rostering/v1p2`. Needs the roster scope (`roster.readonly` or `roster-core.readonly`).
## Endpoints [#endpoints]
Every collection also has a single-record form, `/{collection}/{sourcedId}`, returning the singular object (e.g. `user`).
| Path | Content |
| --- | --- |
| `GET /orgs`, `/schools` | The school |
| `GET /academicSessions`, `/terms`, `/schools/{id}/terms` | Internship seasons |
| `GET /courses`, `/schools/{id}/courses` | The "Thực tập doanh nghiệp" course |
| `GET /classes`, `/terms/{id}/classes`, `/schools/{id}/classes`, `/courses/{id}/classes` | One class per season |
| `GET /users/{id}/classes`, `/students/{id}/classes` | The seasons (classes) a student is in |
| `GET /users`, `/students`, `/schools/{id}/students` | Students and school staff |
| `GET /classes/{id}/students` | A season's students |
| `GET /enrollments`, `/classes/{id}/enrollments`, `/schools/{id}/enrollments` | Students in a season |
### Always empty [#always-empty]
The platform has none of these, so these standard endpoints always return an empty list (200), and a single record by id returns 404:
- `GET /teachers`, `/schools/{id}/teachers`, `/classes/{id}/teachers`, `/teachers/{id}/classes`: no teachers.
- `GET /gradingPeriods`, `/terms/{id}/gradingPeriods`: seasons are not split into grading periods.
- `GET /demographics`: no demographic data is kept.
## Mapping [#mapping]
| Internship data | OneRoster |
| --- | --- |
| School | `orgs` (`type = school`); `identifier` is the school's code on the platform |
| Season | `academicSessions` (`type = term`); `startDate` is when it opened, `endDate` when it closed or is planned to; `metadata."ttdn.open"` tells whether it is open |
| Internship course | `courses` (one, code `TTDN`) and `classes` (one per season) |
| Student | `users` with `roles[].role = student`; student code in `identifier` and `userIds[type = studentCode]`; `email` is `null` when the student has no real email |
| Login name | `username`: the login name or student code for password accounts; the email for Google accounts |
| Name | `familyName` family name, `middleName` middle name, `givenName` given name (Vietnamese order) |
| School staff | `users` with `roles[].role = administrator` |
| Blocked account | `enabledUser = false` |
| Removed account | `status = tobedeleted`, still listed so delta syncs learn about it |
| In a season | `enrollments` (`role = student`) in the season's class; `beginDate` is when the student was added to the season, `endDate` the season's (planned) close |
Companies have no OneRoster role and are not in `users`; the company's name comes with the [internship result](./ket-qua.mdx).
A student's `metadata` also has `ttdn.selfSignup` (signed up with the school's Google account) and `ttdn.studentCodeDeclared` (student code self-declared, not yet confirmed by the school).
# OneRoster overview (/en/thuc-tap-doanh-nghiep/lap-trinh/oneroster)
Each school's portal offers an API that follows the international **1EdTech OneRoster 1.2** standard, so the school's SIS or another system can read students, internship seasons and internship results. Any software that already supports OneRoster can connect without custom work.
## At a glance [#at-a-glance]
- **Standard:** OneRoster 1.2, Rostering and Gradebook services, REST binding, read-only.
- **Authentication:** OAuth 2.0 client credentials, as OneRoster 1.2 requires. See [Authentication](./xac-thuc.mdx).
- **Address:** each school uses its own portal address, e.g. `https://.`. A school's credentials only work there and only see that school's data.
- **Format:** JSON, UTF-8. Timestamps are ISO 8601 (UTC); dates (`startDate`, `endDate`, `scoreDate`) are in Vietnam time.
## Quick start [#quick-start]
### Create a connection [#create-a-connection]
School staff open **Kết nối hệ thống** (Integrations) → **Tạo kết nối** (New connection), name it, pick its scopes, and send the `client_id` and `client_secret` to whoever runs the system. The secret is shown once.
### Get an access token [#get-an-access-token]
```bash
curl -u "CLIENT_ID:CLIENT_SECRET" \
-d "grant_type=client_credentials" \
https://./oauth/token
```
### Call the API [#call-the-api]
```bash
curl -H "Authorization: Bearer ACCESS_TOKEN" \
"https://./ims/oneroster/rostering/v1p2/students?limit=100"
```
## Two services [#two-services]
| Service | Base path | Content |
| --- | --- | --- |
| Rostering | `/ims/oneroster/rostering/v1p2` | School, seasons, course, classes, students, staff, enrollments |
| Gradebook | `/ims/oneroster/gradebook/v1p2` | Each student's internship result in each season |
Details: [Roster data](./du-lieu.mdx) and [Internship results](./ket-qua.mdx). Working code: [Sync examples](./vi-du.mdx).
## Sending data to the portal [#sending-data-to-the-portal]
The OneRoster API is read-only. To send students, seasons and enrollments from the school's systems:
| Option | Use when |
| --- | --- |
| [OneRoster CSV package](../api-cong/goi-oneroster.mdx) | The school's software exports OneRoster CSV packages |
| [Portal API (v1)](../api-cong/index.mdx) | The system does not follow OneRoster and needs a simple JSON API to read and write |
## Machine-readable description [#machine-readable-description]
Each portal serves an OpenAPI 3 file at `/ims/oneroster/openapi.json`, ready for Postman or code generation. The full standard is on the [1EdTech site](https://www.1edtech.org/standards/oneroster).
# Internship results (/en/thuc-tap-doanh-nghiep/lap-trinh/oneroster/ket-qua)
Base path: `/ims/oneroster/gradebook/v1p2`. Needs `gradebook.readonly`.
## Endpoints [#endpoints]
| Path | Content |
| --- | --- |
| `GET /categories` | One "Thực tập" category |
| `GET /lineItems`, `/classes/{id}/lineItems` | One "Kết quả thực tập" line item per season; `assignDate` is when it opened, `dueDate` when it closes |
| `GET /results`, `/lineItems/{id}/results`, `/classes/{id}/results` | One result per student per season |
| `GET /scoreScales`, `/classes/{id}/scoreScales`, `/assessmentLineItems`, `/assessmentResults` | Always empty: the platform does not score |
## Placement status [#placement-status]
| Status | `scoreStatus` | `textScore` |
| --- | --- | --- |
| Accepted by a company | `fully graded` | Đã có nơi thực tập |
| Not placed, an application is waiting | `submitted` | Chưa có nơi thực tập |
| Not placed, nothing waiting | `not submitted` | Chưa có nơi thực tập |
When placed, `comment` is the company's name and `scoreDate` the day of acceptance; `metadata` has `ttdn.placed`, `ttdn.companyId`, `ttdn.companyName` and `ttdn.acceptedAt`. Otherwise `metadata` has `ttdn.placed = false` and `ttdn.pendingApplications`. `score` is always empty: the platform does not grade internships.
A company's acceptance is final, so `fully graded` never changes back.
# Errors and limits (/en/thuc-tap-doanh-nghiep/lap-trinh/oneroster/loi-va-gioi-han)
## Errors [#errors]
Errors use OneRoster's status info shape (`imsx_codeMajor`, `imsx_severity`, `imsx_description`, `imsx_CodeMinor`).
| HTTP | `imsx_codeMinorFieldValue` | When |
| --- | --- | --- |
| 400 | `invalid_filter_field`, `invalid_sort_field`, `invalid_selection_field`, `invalid_data` | Invalid parameter |
| 401 | `unauthorisedrequest` | Missing, wrong or expired token; revoked connection; another school's token |
| 403 | `forbidden` | The token's scopes do not cover the endpoint |
| 404 | `unknownobject` | No such endpoint or record |
| 429 | `server_busy` | Over 600 requests a minute; wait and retry |
| 500 | `internal_server_error` | Server error; retry later and report it if it persists |
| 503 | (none) | Briefly unavailable; retry in a few minutes |
## Limits [#limits]
- 600 requests per minute per connection.
- Token requests: 60 per 15 minutes per network address.
- At most 10 active connections per school.
## Not supported yet [#not-supported-yet]
- Writes over OneRoster REST: the standard has no roster writes, and results are produced by the portal itself. To send data to the portal, use a [OneRoster CSV package](../api-cong/goi-oneroster.mdx) or the [portal API](../api-cong/gui-du-lieu.mdx).
- The Resources service and guardians. Teachers, grading periods and demographics have endpoints that are always empty.
- Webhooks.
# Queries and sync (/en/thuc-tap-doanh-nghiep/lap-trinh/oneroster/truy-van)
## Parameters [#parameters]
| Parameter | Meaning | Example |
| --- | --- | --- |
| `limit`, `offset` | Paging; `limit` 1–1000, default 100 | `limit=200&offset=400` |
| `filter` | `=` `!=` `>` `>=` `<` `<=` `~` (contains); join with `AND` or `OR`; values in single quotes; dotted paths for nested fields | `filter=identifier='SV2026001'` |
| `sort`, `orderBy` | Sort by one field, `asc` or `desc`; text in Vietnamese alphabetical order (Đ after D), numbers in text by value (SV9 before SV10) | `sort=familyName&orderBy=asc` |
| `fields` | Return only these fields | `fields=sourcedId,identifier,email` |
Notes: URL-encode values; one filter uses only `AND` or only `OR`; `~` ignores case; an unknown field in `filter` or `sort` returns 400; `sort` takes top-level fields only, `filter` also takes dotted paths.
## Paging [#paging]
List responses carry `X-Total-Count` (matching records) and `Link` (`first`, `prev`, `next`, `last`).
## Delta sync [#delta-sync]
Every record has `dateLastModified`. Keep the start time of each sync and ask only for what changed since:
```http
GET /ims/oneroster/rostering/v1p2/users?filter=dateLastModified>'2026-11-10T00:00:00.000Z'
```
Removed records are still returned with `status = tobedeleted`. The platform never hard-deletes.
# Ví dụ đồng bộ (/en/thuc-tap-doanh-nghiep/lap-trinh/oneroster/vi-du)
Ví dụ dưới đây làm đủ một vòng đồng bộ: lấy mã truy cập, đọc từng trang danh sách sinh viên, rồi đọc kết quả thực tập. Thay `BASE` bằng địa chỉ cổng của trường, đặt mã ứng dụng và khoá bí mật vào biến môi trường `TTDN_CLIENT_ID`, `TTDN_CLIENT_SECRET`. Không ghi khoá bí mật thẳng vào mã.
```python
import os
import requests
BASE = "https://truong.example.edu.vn" # địa chỉ cổng của trường
CLIENT_ID = os.environ["TTDN_CLIENT_ID"]
CLIENT_SECRET = os.environ["TTDN_CLIENT_SECRET"]
def get_token():
r = requests.post(
f"{BASE}/oauth/token",
auth=(CLIENT_ID, CLIENT_SECRET),
data={"grant_type": "client_credentials"},
timeout=30,
)
r.raise_for_status()
return r.json()["access_token"] # hết hạn sau 1 giờ
def read_all(token, path, key, params=None):
"""Đọc hết mọi trang của một danh sách, 1000 bản ghi mỗi lần."""
rows, offset = [], 0
while True:
r = requests.get(
f"{BASE}/ims/oneroster/{path}",
headers={"Authorization": f"Bearer {token}"},
params={**(params or {}), "limit": 1000, "offset": offset},
timeout=30,
)
r.raise_for_status()
page = r.json()[key]
rows += page
total = int(r.headers.get("X-Total-Count", len(rows)))
offset += len(page)
if not page or offset >= total:
return rows
token = get_token()
students = read_all(token, "rostering/v1p2/students", "users")
results = read_all(token, "gradebook/v1p2/results", "results")
placed = {r["student"]["sourcedId"]: r["comment"] for r in results if r["scoreStatus"] == "fully graded"}
for s in students:
print(s["identifier"], s["familyName"], s["givenName"], "→", placed.get(s["sourcedId"], "chưa có nơi thực tập"))
```
Lưu thành tệp `.mjs` (dùng `await` ở cấp ngoài cùng).
```js
const BASE = "https://truong.example.edu.vn"; // địa chỉ cổng của trường
const { TTDN_CLIENT_ID: id, TTDN_CLIENT_SECRET: secret } = process.env;
async function getToken() {
const res = await fetch(`${BASE}/oauth/token`, {
method: "POST",
headers: {
authorization: "Basic " + Buffer.from(`${id}:${secret}`).toString("base64"),
"content-type": "application/x-www-form-urlencoded",
},
body: "grant_type=client_credentials",
});
if (!res.ok) throw new Error(`token ${res.status}`);
return (await res.json()).access_token; // hết hạn sau 1 giờ
}
/** Đọc hết mọi trang của một danh sách, 1000 bản ghi mỗi lần. */
async function readAll(token, path, key, params = {}) {
const rows = [];
for (let offset = 0; ; ) {
const query = new URLSearchParams({ ...params, limit: "1000", offset: String(offset) });
const res = await fetch(`${BASE}/ims/oneroster/${path}?${query}`, { headers: { authorization: `Bearer ${token}` } });
if (!res.ok) throw new Error(`${path} ${res.status}`);
const page = (await res.json())[key];
rows.push(...page);
offset += page.length;
const total = Number(res.headers.get("x-total-count") ?? rows.length);
if (page.length === 0 || offset >= total) return rows;
}
}
const token = await getToken();
const students = await readAll(token, "rostering/v1p2/students", "users");
const results = await readAll(token, "gradebook/v1p2/results", "results");
const placed = new Map(results.filter((r) => r.scoreStatus === "fully graded").map((r) => [r.student.sourcedId, r.comment]));
for (const s of students) console.log(s.identifier, s.familyName, s.givenName, "→", placed.get(s.sourcedId) ?? "chưa có nơi thực tập");
```
## Đồng bộ định kỳ [#đồng-bộ-định-kỳ]
Sau lần đầu, chỉ cần lấy phần thay đổi. Lưu lại thời điểm **bắt đầu** mỗi lần chạy và truyền vào bộ lọc ở lần sau:
```python
since = "2026-11-10T00:00:00.000Z" # thời điểm bắt đầu lần chạy trước
changed = read_all(token, "rostering/v1p2/users", "users", {"filter": f"dateLastModified>'{since}'"})
```
Bản ghi có `status = "tobedeleted"` là bản ghi đã gỡ ở cổng thực tập: cập nhật hoặc ẩn ở hệ thống của trường.
## Xử lý lỗi [#xử-lý-lỗi]
- `401`: mã hết hạn hoặc kết nối đã bị thu hồi. Xin mã mới; vẫn lỗi thì liên hệ nhà trường.
- `429`: gọi quá 600 lần trong một phút. Đợi khoảng một phút rồi thử lại.
- Các lỗi khác: xem [Lỗi và giới hạn](./loi-va-gioi-han.mdx).
# Authentication (/en/thuc-tap-doanh-nghiep/lap-trinh/oneroster/xac-thuc)
## Getting a token [#getting-a-token]
- **Endpoint:** `POST https://./oauth/token`
- **Body:** `application/x-www-form-urlencoded` (recommended, per OAuth 2.0) or JSON, with `grant_type=client_credentials`.
- **Credentials:** `client_id` and `client_secret` with HTTP Basic (recommended) or in the body.
- **`scope`** (optional): space-separated scopes. Empty means every scope of the connection.
The response carries `Cache-Control: no-store`. Call the API with `Authorization: Bearer `.
## Scopes [#scopes]
| Scope | Grants read access to |
| --- | --- |
| `https://purl.imsglobal.org/spec/or/v1p2/scope/roster-core.readonly` | Every Rostering endpoint |
| `https://purl.imsglobal.org/spec/or/v1p2/scope/roster.readonly` | Every Rostering endpoint |
| `https://purl.imsglobal.org/spec/or/v1p2/scope/gradebook.readonly` | Every Gradebook endpoint |
## Token lifetime [#token-lifetime]
- Tokens last **one hour**. Request a new one when it expires; there are no refresh tokens.
- A token only works at the school that issued it.
- When the school **revokes** a connection, all its tokens stop working at once.
- The school can set an **expiry** (30, 90, 180, 365 days, or none) and limit the **network addresses** allowed to call.
- The secret is shown only when the connection is created. If it is lost, the school presses **Thay khoá** (rotate) to issue a new one; the old one keeps working for 24 hours.
- An **access token** connection (`ttdn_pat_...`) calls the OneRoster API directly with `Authorization: Bearer`, with no token request. See [Portal API](../api-cong/index.mdx#authentication).
Never send the secret by chat or unencrypted email, and never commit it to source code. Keep it in the system's secret store.
## Token errors [#token-errors]
Per OAuth 2.0 (RFC 6749):
| HTTP | `error` | When |
| --- | --- | --- |
| 401 | `invalid_client` | Wrong id or secret, revoked connection, or another school's connection |
| 400 | `unsupported_grant_type` | `grant_type` other than `client_credentials` |
| 400 | `invalid_scope` | A scope the connection does not have |
| 429 | `slow_down` | More than 60 token requests in 15 minutes from one network address |
| 503 | `temporarily_unavailable` | The service is briefly unavailable; retry in a few minutes |
# Đăng nhập và tài khoản (/en/thuc-tap-doanh-nghiep/nguoi-dung/dang-nhap)
Mỗi trường có địa chỉ cổng riêng. Trang **Đăng nhập** có ba thẻ theo vai trò: **Sinh viên**, **Doanh nghiệp**, **Quản trị**. Chọn đúng thẻ của mình; nếu chọn nhầm, trang sẽ báo và có nút chuyển sang thẻ đúng.
## Sinh viên [#sinh-viên]
Tuỳ cách nhà trường cấp tài khoản:
- **Bằng Google của trường:** bấm **Đăng nhập bằng Google của trường** và chọn tài khoản email do trường cấp.
- **Bằng tên đăng nhập và mật khẩu:** nhập tên đăng nhập hoặc mã sinh viên, cùng mật khẩu nhà trường cấp.
Nếu nhà trường cho phép sinh viên tự tạo tài khoản, sinh viên đăng nhập bằng Google lần đầu là có tài khoản, không cần chờ nhập danh sách. Điều kiện: dùng tài khoản Google do trường quản lý, đúng tên miền email nhà trường đã khai, và đang có đợt thực tập mở.
## Doanh nghiệp [#doanh-nghiệp]
Đăng nhập bằng email và mật khẩu đã đăng ký. Doanh nghiệp cần **xác nhận email** trước lần đăng nhập đầu tiên. Chưa có tài khoản thì xem [Đăng ký doanh nghiệp](./doanh-nghiep/dang-ky.mdx).
## Cán bộ nhà trường [#cán-bộ-nhà-trường]
Đăng nhập ở thẻ **Quản trị** bằng email và mật khẩu do đơn vị vận hành cổng cấp.
## Quên mật khẩu [#quên-mật-khẩu]
Bấm **Quên mật khẩu?** ở trang Đăng nhập, nhập email hoặc tên đăng nhập.
Mở email đặt lại mật khẩu. Liên kết dùng được **một lần, trong 1 giờ**. Mỗi tài khoản nhận tối đa 3 email như vậy mỗi giờ.
Nhập mật khẩu mới (từ 8 ký tự) và lưu. Tài khoản bị đăng xuất khỏi mọi thiết bị và nhận email báo đã đổi mật khẩu.
Để bảo vệ tài khoản, trang luôn báo đã gửi, kể cả khi email hay tên đăng nhập không tồn tại. Không nhận được email thì kiểm tra thư rác hoặc liên hệ nhà trường.
Tài khoản đăng nhập bằng Google, hoặc tài khoản sinh viên không có email, không đặt lại mật khẩu qua email được. Hãy liên hệ nhà trường.
## Đổi mật khẩu [#đổi-mật-khẩu]
Khi đã đăng nhập, mở trang thông tin của mình và chọn **Đổi mật khẩu**. Nhập mật khẩu hiện tại và mật khẩu mới (từ 8 ký tự, khác mật khẩu cũ). Tài khoản đăng nhập bằng Google không có mục này.
## Bị tạm chặn đăng nhập [#bị-tạm-chặn-đăng-nhập]
Để chống dò mật khẩu, một tài khoản chỉ được thử đăng nhập 8 lần trong 15 phút, và một mạng chỉ được thử 40 lần trong 15 phút. Vượt quá thì trang báo phải đợi; đợi khoảng 15 phút rồi thử lại.
# Đăng ký tài khoản (/en/thuc-tap-doanh-nghiep/nguoi-dung/doanh-nghiep/dang-ky)
Mở trang **Đăng ký doanh nghiệp** của trường: liên kết ở chân trang, hoặc **Đăng ký ngay** ở thẻ **Doanh nghiệp** trên trang Đăng nhập.
Nhập email, mật khẩu (ít nhất 8 ký tự) và nhập lại mật khẩu. Trang có thể yêu cầu một bước xác minh chống tự động.
Mở email xác nhận và bấm vào liên kết. Trang báo **Đã xác nhận email** là xong; từ đây đăng nhập được ở thẻ **Doanh nghiệp**.
Không thấy email xác nhận? Kiểm tra thư rác, hoặc yêu cầu gửi lại ở trang xác nhận email.
- Không đăng ký được bằng email thuộc tên miền dành cho sinh viên của trường.
- Mỗi email chỉ gửi yêu cầu đăng ký được 3 lần mỗi giờ.
- Để bảo vệ thông tin, trang trả lời giống nhau dù email đã đăng ký hay chưa.
# Hồ sơ doanh nghiệp (/en/thuc-tap-doanh-nghiep/nguoi-dung/doanh-nghiep/ho-so)
Sau khi đăng nhập, mở **Hồ sơ doanh nghiệp** và điền:
- **Tên ngắn** và **Tên đầy đủ**.
- **Địa chỉ**, **Email liên hệ**, **Điện thoại**.
- **Giới thiệu chương trình thực tập**: vị trí, công việc, quyền lợi, yêu cầu.
- **Logo** (không bắt buộc).
Thay đổi trên hồ sơ hiện cho sinh viên ngay. Một số thông tin liên hệ chỉ hiện khi sinh viên đã đăng nhập.
## Chính sách thực tập [#chính-sách-thực-tập]
Tải lên tối đa 5 tệp PDF (dung lượng theo quy định của trường, thường tối đa 10 MB) và đặt tên cho từng tệp. Cần ít nhất một tệp để được duyệt; khi đã được duyệt, không xoá được tệp cuối cùng.
# Tổng quan cho doanh nghiệp (/en/thuc-tap-doanh-nghiep/nguoi-dung/doanh-nghiep)
Mỗi trường có cổng thực tập riêng. Doanh nghiệp muốn nhận sinh viên của trường nào thì đăng ký trên cổng của trường đó.
**Đăng ký tài khoản** và xác nhận email. Xem [Đăng ký tài khoản](./dang-ky.mdx).
**Hoàn thiện hồ sơ doanh nghiệp**, gồm tệp chính sách thực tập. Xem [Hồ sơ doanh nghiệp](./ho-so.mdx).
**Gửi hồ sơ tham gia đợt** để nhà trường duyệt. Xem [Tham gia đợt thực tập](./tham-gia-dot.mdx).
**Xét hồ sơ sinh viên**: nhận hoặc từ chối. Xem [Xét hồ sơ ứng tuyển](./xet-ho-so.mdx).
# Tham gia đợt thực tập (/en/thuc-tap-doanh-nghiep/nguoi-dung/doanh-nghiep/tham-gia-dot)
Mỗi đợt thực tập, doanh nghiệp gửi hồ sơ tham gia để nhà trường duyệt.
Điền đủ thông tin bắt buộc trong hồ sơ (tên, địa chỉ, email liên hệ, điện thoại, giới thiệu) và tải lên chính sách thực tập.
Bấm **Gửi hồ sơ duyệt**. Nhà trường nhận bản hồ sơ tại thời điểm gửi.
Chờ nhà trường duyệt. Được duyệt, doanh nghiệp hiện trên trang chủ của trường và bắt đầu nhận hồ sơ.
Bị từ chối thì xem **Lý do của nhà trường**, sửa hồ sơ và **Gửi lại hồ sơ** trong cùng đợt. Khi nhà trường mở đợt mới, doanh nghiệp gửi lại hồ sơ cho đợt đó.
## Chỉ tiêu và nhận hồ sơ [#chỉ-tiêu-và-nhận-hồ-sơ]
Nhà trường đặt hai chỉ tiêu cho mỗi doanh nghiệp: số hồ sơ chờ tối đa và số sinh viên tiếp nhận tối đa. Doanh nghiệp thấy trạng thái **Đang nhận đơn** hoặc **Đã đóng nhận đơn**, nhưng không tự mở hay đóng được.
- Đủ số sinh viên tiếp nhận thì hệ thống tự đóng nhận hồ sơ.
- Đủ số hồ sơ chờ thì sinh viên tạm không nộp thêm được cho tới khi doanh nghiệp trả lời bớt.
Khi đã đóng nhận hồ sơ, doanh nghiệp không nhận thêm sinh viên được nữa; các hồ sơ còn chờ chỉ có thể **Từ chối**. Nên trả lời hết hồ sơ trước khi đủ chỉ tiêu hoặc trước khi nhà trường đóng tuyển.
# Xét hồ sơ ứng tuyển (/en/thuc-tap-doanh-nghiep/nguoi-dung/doanh-nghiep/xet-ho-so)
Mở **Hồ sơ ứng tuyển**. Lọc theo **Chờ duyệt**, **Đã nhận**, **Từ chối**, **Đã đóng**; xem đợt này hoặc mọi đợt; tìm theo tên hoặc mã sinh viên. Mỗi hồ sơ có thông tin sinh viên tại lúc nộp và **Xem CV**.
## Nhận hoặc từ chối [#nhận-hoặc-từ-chối]
Bấm **Nhận sinh viên** hoặc **Từ chối**.
Điền lý do (bắt buộc khi từ chối), nhận xét về CV và ghi chú nếu cần (mỗi ô tối đa 1.000 ký tự), rồi bấm **Tiếp tục**.
Bấm **Chốt nhận** hoặc **Gửi từ chối**. Sinh viên nhận email báo kết quả.
Cả hai quyết định đều **không thay đổi được**. Sinh viên được nhận là có nơi thực tập ngay, không cần xác nhận thêm.
## Khi nhận một sinh viên [#khi-nhận-một-sinh-viên]
Các hồ sơ khác của sinh viên đó tự chuyển sang **Đã đóng**, và các doanh nghiệp liên quan nhận email báo.
## Hồ sơ Đã đóng [#hồ-sơ-đã-đóng]
Là hồ sơ không còn xét được: sinh viên đã có nơi thực tập khác, hoặc bị nhà trường khoá tài khoản. Doanh nghiệp nhận email báo trong cả hai trường hợp.
Hệ thống không gửi email khi có hồ sơ mới; hãy vào trang **Hồ sơ ứng tuyển** thường xuyên.
# Cài đặt trường (/en/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/cai-dat)
Mở **Cài đặt trường**. Trang chia thành bốn thẻ; sửa ở nhiều thẻ rồi bấm **Lưu thay đổi** một lần (hoặc **Hoàn tác**). Riêng logo và biểu tượng trang được lưu ngay khi tải lên. Thay đổi áp dụng ngay cho mọi người dùng của trường.
## Nhận diện [#nhận-diện]
- **Tên trang** (2 đến 80 ký tự): hiện ở đầu trang và trên thẻ trình duyệt.
- **Màu chính**, **Màu nhấn**, **Màu nền**, có phần xem trước.
- **Logo trường** và **Biểu tượng trang** (hình nhỏ trên thẻ trình duyệt). Chưa có biểu tượng riêng thì dùng logo trường.
## Liên hệ và email [#liên-hệ-và-email]
- **Email liên hệ**: người dùng trả lời email của hệ thống sẽ gửi tới địa chỉ này.
- **Số điện thoại**, **Địa chỉ**: hiện ở cuối mỗi email.
- **Lời nhắn cuối email** (tối đa 300 ký tự), ví dụ giờ làm việc.
- **Gửi email thử** để xem email mẫu mang tên, logo và màu của trường.
## Tài khoản và giới hạn [#tài-khoản-và-giới-hạn]
- **Sinh viên tự tạo tài khoản bằng Google**: xem [Sinh viên tự đăng ký](./sinh-vien-tu-dang-ky.mdx).
- **Số đơn chờ tối đa mỗi sinh viên**: 1 đến 10, mặc định 3.
- **Dung lượng tối đa mỗi tệp (MB)**: 1 đến 10, mặc định 10. Áp dụng cho CV, logo và tệp chính sách thực tập.
## Trang trợ giúp [#trang-trợ-giúp]
Soạn **Hướng dẫn** và **Quy định** hiện ở trang Trợ giúp của trường cho mọi người dùng. Thanh công cụ có định dạng chữ, căn lề, chèn liên kết và ảnh.
# Đơn ứng tuyển (/en/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/don-ung-tuyen)
Trang **Đơn ứng tuyển** liệt kê mọi hồ sơ của đợt đang mở. Nhà trường chỉ theo dõi; việc nhận hay từ chối do doanh nghiệp quyết định.
- **Lọc** theo trạng thái (đang chờ, được nhận, bị từ chối, đã đóng) và theo doanh nghiệp; tìm kiếm theo tên.
- **Gom theo sinh viên** để xem mỗi sinh viên đã nộp vào đâu.
- **Xem CV** của từng hồ sơ.
- **Xuất CSV** để lưu hoặc báo cáo.
Muốn biết sinh viên nào chưa có nơi thực tập, xem trang **Tổng quan** hoặc lọc hồ sơ theo trạng thái.
# Đợt thực tập (/en/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/dot-thuc-tap)
Mọi hoạt động diễn ra trong **đợt đang mở**: doanh nghiệp gửi hồ sơ tham gia, sinh viên nộp hồ sơ ứng tuyển. Mỗi lúc chỉ có một đợt mở. Các việc dưới đây làm ở trang **Tổng quan**.
## Mở đợt mới [#mở-đợt-mới]
Bấm **Mở đợt mới**.
Nhập **Tên đợt** (2 đến 80 ký tự), ví dụ "Đợt thực tập học kỳ 2 năm 2026".
Nếu muốn, chọn **Tự đóng lúc**: thời điểm đợt tự đóng, sớm nhất 5 phút sau, muộn nhất 2 năm sau. Để trống thì nhà trường tự đóng.
Mở đợt mới sẽ **đóng ngay đợt đang mở**. Doanh nghiệp phải gửi lại hồ sơ tham gia và nhà trường phải đưa lại danh sách sinh viên cho đợt mới.
## Hẹn giờ đóng [#hẹn-giờ-đóng]
Bấm **Hẹn giờ đóng** (hoặc **Đổi giờ đóng** nếu đã hẹn), chọn thời điểm rồi **Lưu giờ đóng**. Muốn bỏ thì bấm **Bỏ hẹn giờ**. Đến giờ, đợt tự đóng như khi bấm đóng tay.
## Đóng đợt [#đóng-đợt]
Bấm **Đóng đợt này** rồi xác nhận. Từ lúc đó sinh viên không nộp thêm hồ sơ và doanh nghiệp không nhận thêm sinh viên.
- Đợt đã đóng **không mở lại được**. Các đợt cũ vẫn xem được ở mục **Các đợt trước**.
- Hồ sơ còn đang chờ sẽ giữ nguyên trạng thái chờ, không ai xử lý tiếp được nữa. Nên nhắc doanh nghiệp trả lời hết trước khi đóng.
# Duyệt doanh nghiệp (/en/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/duyet-doanh-nghiep)
Doanh nghiệp muốn xuất hiện trên trang chủ của trường phải gửi hồ sơ tham gia cho đợt đang mở và được nhà trường duyệt.
## Tìm hồ sơ cần duyệt [#tìm-hồ-sơ-cần-duyệt]
Mở trang **Doanh nghiệp**. Các nhóm: **Chờ duyệt**, **Đã duyệt**, **Bị từ chối**, **Chưa gửi**, **Tất cả**. Bấm vào một doanh nghiệp để mở phần chi tiết, gồm:
- **Hồ sơ đã gửi**: bản doanh nghiệp gửi để duyệt, giữ nguyên tại thời điểm gửi.
- **Hồ sơ hiện tại**: thông tin doanh nghiệp đang hiện cho sinh viên.
- **Chính sách thực tập** (tệp PDF).
## Duyệt [#duyệt]
**Đặt chỉ tiêu**: số hồ sơ chờ tối đa và số sinh viên tiếp nhận tối đa của doanh nghiệp trong đợt, rồi bấm **Lưu chỉ tiêu**.
**Kiểm tra điều kiện duyệt**: đủ tên ngắn và tên đầy đủ, đã đặt chỉ tiêu, có ít nhất một tệp chính sách thực tập, đã gửi hồ sơ cho đợt đang mở.
Bấm **Duyệt**. Doanh nghiệp hiện ngay trên trang chủ và bắt đầu nhận hồ sơ.
## Từ chối [#từ-chối]
Bấm **Từ chối** và ghi lý do (không bắt buộc nhưng nên có). Doanh nghiệp thấy lý do, sửa hồ sơ và có thể gửi lại trong cùng đợt.
Nhà trường không gửi email báo kết quả duyệt; doanh nghiệp xem kết quả khi đăng nhập.
# Tổng quan cho nhà trường (/en/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong)
Cán bộ nhà trường đăng nhập ở thẻ **Quản trị** và dùng các mục trên thanh điều hướng: **Tổng quan**, **Doanh nghiệp**, **Sinh viên**, **Đơn ứng tuyển**, **Nhật ký**, **Cài đặt trường**, **Kết nối hệ thống**.
## Một đợt thực tập, từ đầu đến cuối [#một-đợt-thực-tập-từ-đầu-đến-cuối]
**Chuẩn bị cổng** (làm một lần): tên, màu, logo, thông tin liên hệ, trang trợ giúp. Xem [Cài đặt trường](./cai-dat.mdx).
**Mở đợt thực tập** và hẹn giờ đóng nếu muốn. Xem [Đợt thực tập](./dot-thuc-tap.mdx).
**Đưa sinh viên vào đợt**: nhập từ tệp CSV hoặc thêm từng người. Xem [Nhập danh sách sinh viên](./nhap-sinh-vien.mdx). Phần mềm đào tạo của trường xuất được gói OneRoster thì [tải gói lên](./nhan-goi-oneroster.mdx), hoặc [kết nối](./ket-noi-he-thong.mdx) để phần mềm tự gửi.
**Duyệt doanh nghiệp** gửi hồ sơ tham gia đợt. Xem [Duyệt doanh nghiệp](./duyet-doanh-nghiep.mdx).
**Theo dõi** hồ sơ ứng tuyển và tình hình sinh viên có nơi thực tập. Xem [Đơn ứng tuyển](./don-ung-tuyen.mdx).
**Đóng đợt** khi kết thúc.
Trang **Tổng quan** cho biết đợt đang mở, các số liệu chính và **Việc cần làm** (ví dụ doanh nghiệp đang chờ duyệt, đợt chưa có sinh viên).
# Kết nối hệ thống (/en/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/ket-noi-he-thong)
Trang **Kết nối hệ thống** dùng khi trường muốn phần mềm quản lý đào tạo (hoặc hệ thống khác) tự **lấy** danh sách và kết quả thực tập, hoặc tự **gửi** sinh viên, đợt thực tập vào cổng, thay vì cán bộ nhập tay.
## Tạo kết nối [#tạo-kết-nối]
Bấm **Tạo kết nối**.
Trả lời **Hệ thống sẽ kết nối là loại nào?**:
- **Phần mềm theo chuẩn OneRoster**: phần mềm của trường có ghi hỗ trợ OneRoster. Cổng cấp mã ứng dụng và khoá bí mật.
- **Hệ thống khác (mã truy cập)**: đơn vị phụ trách tự viết phần kết nối, hoặc chưa chắc. Cổng cấp một mã truy cập.
Chưa rõ thì hỏi đơn vị phụ trách hệ thống; chọn sai cũng không sao, có thể tạo kết nối khác.
Đặt **Tên kết nối** (2 đến 80 ký tự) để biết hệ thống nào đang dùng, ví dụ “Phần mềm đào tạo”.
Xem các việc hệ thống **Được phép** làm. Cổng đã chọn sẵn theo loại kết nối; chỉ thêm những gì thật sự cần. Bấm **Chọn mọi quyền lấy dữ liệu**, **Chọn tất cả** hoặc **Bỏ chọn** để chọn nhanh.
- **Lấy dữ liệu theo chuẩn OneRoster**: **Danh sách**, **Kết quả thực tập**.
- **Lấy dữ liệu từ cổng**: sinh viên, đợt thực tập, sinh viên trong đợt, kết quả thực tập, doanh nghiệp đã duyệt.
- **Gửi dữ liệu vào cổng**: thêm, sửa, khoá sinh viên; tạo, cập nhật đợt; đưa sinh viên vào đợt.
- **Gửi gói OneRoster**: hệ thống tự gửi gói CSV OneRoster.
Chọn **Thời hạn** (30 ngày, 90 ngày, 180 ngày, 1 năm hoặc **Không hết hạn**). Nếu biết địa chỉ mạng của máy chủ sẽ gọi, nhập vào **Chỉ nhận từ các địa chỉ**, mỗi dòng một địa chỉ; để trống thì nhận từ mọi nơi.
Bấm **Tạo kết nối**, sao chép thông tin hiện ra (mã ứng dụng và khoá bí mật, hoặc mã truy cập) và gửi cho đơn vị phụ trách hệ thống qua kênh an toàn. Bấm **Đã lưu lại, đóng**.
Khoá bí mật và mã truy cập **chỉ hiện một lần**. Nếu làm mất, dùng **Thay khoá** để cấp mã mới.
Khung **Địa chỉ kết nối** luôn hiện các địa chỉ cần gửi kèm: địa chỉ API của cổng, địa chỉ lấy mã truy cập, hai địa chỉ OneRoster và tệp mô tả API.
## Theo dõi [#theo-dõi]
Mỗi kết nối hiện loại, quyền, thời hạn, lần dùng gần nhất (kèm địa chỉ mạng) và người tạo. Bấm **Theo dõi** để xem:
- số lần gọi mỗi ngày trong 30 ngày, chia theo **Thành công**, **Lỗi do yêu cầu**, **Lỗi hệ thống**;
- tổng số lần gọi và số bản ghi hệ thống đã gửi lên, theo từng loại thao tác (ví dụ **Xem sinh viên**, **Gửi danh sách sinh viên**);
- các lỗi gần đây kèm lý do dễ hiểu (ví dụ “Mã không đúng, đã hết hạn hoặc đã thu hồi”). Nhiều lỗi lặp lại thì báo đơn vị phụ trách hệ thống.
Cổng chỉ đếm số lượng, không lưu nội dung dữ liệu hệ thống gửi.
## Thay khoá [#thay-khoá]
Bấm **Thay khoá** khi khoá bị lộ, bị mất, hoặc theo định kỳ. Cổng cấp khoá mới ngay (chỉ hiện một lần); khoá cũ vẫn dùng được **24 giờ** để hệ thống kịp đổi, trong thời gian đó kết nối cũ hiện **Đang thay khoá**.
## Thu hồi [#thu-hồi]
Bấm **Thu hồi** để ngừng ngay một kết nối, ví dụ khi thôi dùng phần mềm hoặc nghi bị lộ khoá. Việc này **không hoàn tác được**; muốn dùng tiếp phải tạo kết nối mới. Mỗi trường có tối đa 10 kết nối đang hoạt động.
## Dữ liệu hệ thống gửi vào [#dữ-liệu-hệ-thống-gửi-vào]
Dữ liệu hệ thống gửi vào (sinh viên, đợt, ghi danh, khoá tài khoản) được ghi vào **Nhật ký**, người thực hiện là tên kết nối, và hiện ở mục **Lịch sử nhận dữ liệu** cùng trang. Lượt nào có dòng lỗi thì bấm **Tải danh sách lỗi (CSV)** để xem. Lượt đang chờ hoặc đang xử lý có thể bấm **Huỷ**.
Cổng không xoá sinh viên theo dữ liệu gửi vào, không đổi email, mật khẩu hay cách đăng nhập của tài khoản đã có, và không tự đóng đợt đang mở. Khi hệ thống của trường đã sang kỳ mới, trang hiện nút **Chuyển sang đợt** để nhà trường quyết định; xem [Nhận gói OneRoster](./nhan-goi-oneroster.mdx#chuyển-sang-đợt-mới).
Tài liệu cho đơn vị phụ trách hệ thống: [Tổng quan API](../../lap-trinh/oneroster/index.mdx) và [API của cổng](../../lap-trinh/api-cong/index.mdx).
# Nhận gói OneRoster (/en/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/nhan-goi-oneroster)
Nhiều phần mềm quản lý đào tạo xuất được **gói CSV OneRoster** (một tệp `.zip`) chứa kỳ học, sinh viên và ghi danh. Tải gói lên cổng thay cho việc [nhập sinh viên](./nhap-sinh-vien.mdx) bằng tệp Excel.
## Tải gói lên [#tải-gói-lên]
Vào **Kết nối hệ thống**, ở mục **Nhận dữ liệu từ gói OneRoster** bấm **Chọn tệp .zip** và chọn gói (tối đa 20 MB).
Bấm **Kiểm tra gói**. Cổng đọc gói và cho xem trước; **chưa thay đổi gì**.
Xem **Kết quả kiểm tra**: số đợt mới, sinh viên mới, sinh viên được cập nhật, sinh viên được đưa vào đợt, tài khoản bị khoá, dòng lỗi. Có dòng lỗi thì bấm **Tải danh sách lỗi (CSV)** để xem lý do.
Bấm **Áp dụng**. Cổng xử lý dần ở nền; trang hiện tiến độ và báo **Đã áp dụng** khi xong.
Kết quả kiểm tra giữ được một thời gian; nếu quá lâu, cổng báo cần tải lại gói.
## Cổng làm gì với gói [#cổng-làm-gì-với-gói]
- Mỗi **kỳ học** trong gói thành một đợt thực tập. Kỳ đã qua thành đợt đã đóng (để lưu lịch sử); kỳ chưa tới thành đợt chưa mở.
- Mọi người có vai trò **sinh viên** trong gói là sinh viên thực tập. Giáo viên, phụ huynh và người khác được bỏ qua.
- Sinh viên đã có trên cổng được cập nhật họ tên và mã; **email, mật khẩu, cách đăng nhập giữ nguyên**. Sinh viên mới có mật khẩu trong gói thì đăng nhập bằng tên đăng nhập, không có thì đăng nhập bằng Google.
- Sinh viên được đưa vào đợt của kỳ mình ghi danh. Gói không có tệp ghi danh thì sinh viên được đưa vào đợt đang mở.
- Sinh viên bị đánh dấu xoá hoặc ngừng hoạt động trong gói thì bị **khoá**.
- Sinh viên **không có trong gói được giữ nguyên**. Cổng không xoá ai theo gói.
## Chuyển sang đợt mới [#chuyển-sang-đợt-mới]
Khi hệ thống của trường đã sang kỳ mới mà cổng vẫn đang mở đợt cũ, cổng **không tự đóng** đợt cũ. Trang báo “Hệ thống của trường đang ở kỳ … còn cổng đang mở đợt …” cùng nút **Chuyển sang đợt**.
Bấm **Áp dụng** trước để cổng tạo đợt mới.
Bấm **Chuyển sang đợt “…”** và đọc kỹ hộp xác nhận.
Bấm xác nhận. Đợt cũ đóng, đợt mới mở.
Đợt đã đóng **không mở lại được**. Doanh nghiệp phải gửi lại hồ sơ tham gia cho đợt mới, như khi [mở đợt mới](./dot-thuc-tap.mdx) bằng tay.
## Lịch sử nhận dữ liệu [#lịch-sử-nhận-dữ-liệu]
Mục **Lịch sử nhận dữ liệu** liệt kê các lần nhận gần đây: gói do cán bộ tải lên (lần **Xem trước** và lần áp dụng) và dữ liệu hệ thống gửi qua API, kèm người gửi, thời điểm, số mới, số cập nhật, số lỗi. Lần đang chờ hoặc đang xử lý có thể bấm **Huỷ**; phần đã xử lý vẫn được giữ.
# Nhập danh sách sinh viên (/en/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/nhap-sinh-vien)
Chỉ sinh viên có tên trong đợt đang mở mới nộp hồ sơ được. Nhập hay thêm sinh viên cần có đợt đang mở. Các việc dưới đây làm ở trang **Sinh viên**.
## Nhập từ tệp CSV [#nhập-từ-tệp-csv]
Bấm **Nhập từ CSV**, rồi **Tải tệp CSV mẫu** để có tệp đúng định dạng.
Điền danh sách vào tệp (lưu dạng CSV, mã hoá UTF-8; tối đa 20 MB và 50.000 dòng).
Chọn tệp, hoặc dán nội dung vào ô **Hoặc dán nội dung CSV**, rồi bắt đầu nhập. Danh sách dài được gửi thành nhiều đợt nhỏ; có thể **Dừng sau đợt này** và **Tiếp tục phần còn lại**.
Xem kết quả. Dòng lỗi được liệt kê kèm lý do; bấm **Tải danh sách lỗi (CSV)** để sửa rồi nhập lại riêng các dòng đó.
### Các cột [#các-cột]
| Cột | Bắt buộc | Ghi chú |
| --- | --- | --- |
| `studentCode` | Có | Mã sinh viên, tối đa 40 ký tự |
| `email` | Khi đăng nhập bằng Google | Email do trường cấp; cũng là nơi nhận email thông báo |
| `name` | Có | Họ và tên, tối đa 120 ký tự |
| `username` | Không | Tên đăng nhập (3 đến 64 ký tự: chữ không dấu, số, dấu chấm, gạch dưới, gạch ngang); mặc định là mã sinh viên; chỉ dùng khi có `password` |
| `password` | Không | Từ 8 ký tự; để trống thì sinh viên đăng nhập bằng Google |
- **Có dòng tiêu đề** (dòng đầu có chữ `studentCode`): các cột đặt theo thứ tự nào cũng được.
- **Không có dòng tiêu đề:** các cột phải theo đúng thứ tự `studentCode, email, name, username, password`.
```csv title="sinh-vien.csv"
studentCode,email,name,username,password
SV2026001,an.nv@truong.edu.vn,Nguyễn Văn An,,
SV2026002,,Trần Thị Bình,sv2026002,MatKhau@2026
```
Dòng đầu đăng nhập bằng Google; dòng thứ hai đăng nhập bằng tên đăng nhập và mật khẩu.
### Sinh viên đã có tài khoản [#sinh-viên-đã-có-tài-khoản]
Được đưa vào đợt và cập nhật họ tên, mã theo tệp. Cách đăng nhập giữ nguyên: cột `username`, `password` được bỏ qua.
## Thêm một sinh viên [#thêm-một-sinh-viên]
Bấm **Thêm sinh viên**.
Nhập **Mã sinh viên**, **Họ và tên**, **Email**.
Chọn **Cách đăng nhập**: **Google của trường**, hoặc **Tên đăng nhập và mật khẩu** (nhập mật khẩu ban đầu hoặc bấm **Tạo ngẫu nhiên**).
Hệ thống không gửi email cho sinh viên mới thêm; nhà trường tự gửi thông tin đăng nhập cho sinh viên.
# Nhật ký (/en/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/nhat-ky)
Trang **Nhật ký** có hai thẻ.
## Hoạt động [#hoạt-động]
Ghi lại mọi thao tác quan trọng: ai làm, làm gì, lúc nào. Lọc theo **Từ ngày**, **Đến ngày** (mặc định 30 ngày gần nhất), **Nhóm**, **Người thực hiện**, hoặc tìm kiếm. Bấm một dòng để xem chi tiết. **Xuất CSV** để lưu (tối đa 10.000 dòng mỗi lần).
## Email [#email]
Danh sách email hệ thống đã gửi, với trạng thái **Đang chờ gửi**, **Đã gửi** hoặc **Gửi lỗi**. Xem lại nội dung từng email và bấm **Gửi lại** nếu cần.
- Email gửi lỗi được tự thử lại tối đa 5 lần.
- Email đặt lại mật khẩu và xác nhận email chỉ được giữ 7 ngày.
# Quản lý doanh nghiệp trong đợt (/en/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/quan-ly-doanh-nghiep)
Với doanh nghiệp đã duyệt, mở phần chi tiết ở trang **Doanh nghiệp** để làm các việc sau.
## Đổi chỉ tiêu [#đổi-chỉ-tiêu]
Sửa số hồ sơ chờ tối đa hoặc số sinh viên tiếp nhận tối đa, rồi **Lưu chỉ tiêu**.
## Đóng tuyển [#đóng-tuyển]
Bấm **Đóng tuyển** khi doanh nghiệp không nhận thêm sinh viên trong đợt. Đủ số sinh viên tiếp nhận thì hệ thống tự đóng tuyển.
Đóng tuyển **không mở lại được** trong đợt. Sau khi đóng, doanh nghiệp chỉ còn **Từ chối** được các hồ sơ đang chờ, không nhận thêm ai.
## Tạm khoá doanh nghiệp [#tạm-khoá-doanh-nghiệp]
Ở mục **Hiển thị trên trang chủ**, bỏ chọn **Cho hiển thị (không khoá)** rồi bấm **Lưu hiển thị**. Doanh nghiệp biến khỏi trang chủ, hiện nhãn **Đang khoá** và không nhận thêm hồ sơ. Chọn lại để mở khoá.
## Nhà tài trợ [#nhà-tài-trợ]
Bật **Nhà tài trợ** để doanh nghiệp có trang giới thiệu riêng (thay vì hộp thoại chi tiết) và hiện ở nhóm **Nhà tài trợ** trên trang chủ.
# Quản lý sinh viên (/en/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/quan-ly-sinh-vien)
Trang **Sinh viên** liệt kê sinh viên của trường. Cột **Quyền** cho biết tình trạng:
- **Được ứng tuyển**: có tên trong đợt đang mở.
- **Chỉ xem**: chưa có tên trong đợt đang mở, vào được cổng nhưng không nộp hồ sơ được.
- **Bị khoá**: không đăng nhập được.
Danh sách xuất được ra tệp bằng **Xuất CSV**.
## Sửa họ tên, mã sinh viên [#sửa-họ-tên-mã-sinh-viên]
Chọn **Sửa họ tên, mã sinh viên** ở dòng của sinh viên. Lưu xong, mã sinh viên được xem là đã xác nhận (kể cả mã sinh viên tự khai).
## Sửa đăng nhập [#sửa-đăng-nhập]
Chọn **Sửa đăng nhập** để đổi giữa Google và mật khẩu, đổi email, tên đăng nhập hoặc đặt mật khẩu mới. Sinh viên bị đăng xuất khỏi mọi thiết bị và nhận email báo.
## Khoá và mở khoá [#khoá-và-mở-khoá]
Chọn **Khoá** (hoặc chọn nhiều sinh viên, tối đa 100 một lúc). Khi khoá:
- Sinh viên bị đăng xuất và không đăng nhập được.
- Các hồ sơ đang chờ bị rút; sinh viên và các doanh nghiệp liên quan nhận email báo.
- Nơi thực tập đã được nhận vẫn giữ.
**Mở khoá** để sinh viên dùng lại tài khoản; sinh viên nhận email báo. Hồ sơ đã bị rút không tự quay lại.
# Sinh viên tự đăng ký (/en/thuc-tap-doanh-nghiep/nguoi-dung/nha-truong/sinh-vien-tu-dang-ky)
Thay vì nhập danh sách, nhà trường có thể cho sinh viên tự tạo tài khoản khi đăng nhập bằng tài khoản Google do trường cấp.
## Bật tính năng [#bật-tính-năng]
Mở **Cài đặt trường**, thẻ **Tài khoản và giới hạn**.
Ở **Tên miền email của trường**, thêm tên miền email sinh viên, ví dụ `@sv.truong.edu.vn` (tối đa 5; không nhận email công cộng như gmail.com).
Chọn cách xử lý **Khi có sinh viên mới đăng nhập**, rồi **Lưu thay đổi**.
| Lựa chọn | Điều xảy ra |
| --- | --- |
| **Không tạo tài khoản** | Chỉ sinh viên đã có trong danh sách mới đăng nhập được |
| **Tạo tài khoản, chờ trường duyệt** | Sinh viên vào được cổng nhưng chỉ nộp hồ sơ sau khi nhà trường duyệt |
| **Tạo tài khoản và cho ứng tuyển ngay** | Sinh viên được đưa vào đợt đang mở ngay lần đăng nhập đầu; ở các đợt sau cũng tự vào khi đăng nhập |
Điều kiện: nền tảng đã bật đăng nhập Google, sinh viên dùng tài khoản Google do trường quản lý, và đang có đợt mở.
## Duyệt sinh viên tự đăng ký [#duyệt-sinh-viên-tự-đăng-ký]
Ở trang **Sinh viên**, bộ lọc **Chờ duyệt** liệt kê sinh viên đã tự tạo tài khoản nhưng chưa có tên trong đợt. Bấm **Duyệt** (làm được cho nhiều sinh viên một lúc) để đưa vào đợt; sinh viên nhận email báo và mã sinh viên tự khai được xác nhận.
## Rà soát mã sinh viên [#rà-soát-mã-sinh-viên]
Sinh viên tự đăng ký tự khai mã sinh viên. Hai bộ lọc giúp kiểm tra:
- **Chưa có MSSV**: sinh viên chưa khai mã, nên chưa nộp hồ sơ được.
- **MSSV tự khai**: mã do sinh viên khai, chưa được nhà trường xác nhận.
Sửa mã bằng **Sửa họ tên, mã sinh viên** để xác nhận.
# Chuẩn bị hồ sơ (/en/thuc-tap-doanh-nghiep/nguoi-dung/sinh-vien/chuan-bi)
Mở **Thông tin cá nhân** và kiểm tra các mục sau.
## Bắt buộc để nộp hồ sơ [#bắt-buộc-để-nộp-hồ-sơ]
- **Số điện thoại:** 10 hoặc 11 chữ số, không trùng với sinh viên khác của trường.
- **Mã sinh viên:** thường do nhà trường nhập sẵn. Nếu bạn tự tạo tài khoản bằng Google, bạn tự khai mã ở đây (3 đến 40 ký tự). Mã khoá lại khi nhà trường xác nhận hoặc khi bạn nộp hồ sơ đầu tiên, nên hãy khai đúng.
- **CV:** một tệp PDF, tải lên lúc nộp hồ sơ (dung lượng tối đa theo quy định của trường, thường là 10 MB).
## Nên có [#nên-có]
- **Email nhận thông tin:** gửi kèm hồ sơ để doanh nghiệp liên hệ với bạn.
- **Ngành học**, **Lớp**, **Kỹ năng**, **Giới thiệu bản thân**, liên kết tới sản phẩm hoặc trang cá nhân.
Doanh nghiệp xem các thông tin này cùng CV khi xét hồ sơ. Họ tên, email đăng nhập và tên đăng nhập do nhà trường quản lý, bạn không tự sửa được.
## Tình trạng của bạn [#tình-trạng-của-bạn]
Trang **Thông tin cá nhân** cho biết số hồ sơ **Đang chờ**, **Được nhận**, **Tổng số đơn**, và tình trạng: **Đã có nơi thực tập**, **Chưa có nơi thực tập**, hoặc **Chưa có tên trong đợt đang mở**.
# Tổng quan cho sinh viên (/en/thuc-tap-doanh-nghiep/nguoi-dung/sinh-vien)
**Đăng nhập** bằng tài khoản nhà trường cấp. Xem [Đăng nhập và tài khoản](../dang-nhap.mdx).
**Chuẩn bị hồ sơ**: số điện thoại, mã sinh viên, CV. Xem [Chuẩn bị hồ sơ](./chuan-bi.mdx).
**Tìm doanh nghiệp** phù hợp. Xem [Tìm doanh nghiệp](./tim-doanh-nghiep.mdx).
**Nộp hồ sơ** và chờ doanh nghiệp trả lời. Xem [Nộp hồ sơ](./nop-ho-so.mdx) và [Kết quả](./ket-qua.mdx).
Chỉ sinh viên có tên trong đợt thực tập đang mở mới nộp hồ sơ được. Trang **Thông tin cá nhân** báo **Chưa có tên trong đợt đang mở** thì hãy liên hệ nhà trường.
# Kết quả (/en/thuc-tap-doanh-nghiep/nguoi-dung/sinh-vien/ket-qua)
## Được nhận [#được-nhận]
Bạn có nơi thực tập ngay, không cần xác nhận thêm. Các hồ sơ khác đang chờ của bạn tự chuyển sang **Đã đóng**, và bạn không nộp thêm hồ sơ được.
## Bị từ chối [#bị-từ-chối]
Bạn nhận email kèm lý do. Hồ sơ đó không còn tính vào giới hạn, bạn có thể nộp vào doanh nghiệp khác.
## Email bạn nhận [#email-bạn-nhận]
Email gửi tới **email của tài khoản** (email dùng để đăng nhập hoặc email nhà trường đã nhập), khi:
- Doanh nghiệp nhận hoặc từ chối hồ sơ.
- Nhà trường duyệt tài khoản tự đăng ký, khoá hoặc mở khoá tài khoản, hay đổi cách đăng nhập.
- Bạn đặt lại mật khẩu.
Tài khoản không có email sẽ không nhận được email; hãy theo dõi ở trang **Thông tin cá nhân**.
# Nộp hồ sơ (/en/thuc-tap-doanh-nghiep/nguoi-dung/sinh-vien/nop-ho-so)
Ở doanh nghiệp muốn ứng tuyển, bấm **Nộp hồ sơ**.
Chọn CV: **Dùng lại CV gần nhất** hoặc **Tải CV mới** (tệp PDF).
Bấm **Tiếp tục**, kiểm tra lại, rồi **Xác nhận nộp**.
Hồ sơ đã nộp **không sửa và không rút lại được**. Kiểm tra kỹ CV trước khi xác nhận.
## Nộp được bao nhiêu hồ sơ [#nộp-được-bao-nhiêu-hồ-sơ]
Mỗi lúc bạn chỉ có một số hồ sơ **đang chờ** theo giới hạn của nhà trường (thường là 3). Hồ sơ được trả lời (nhận hoặc từ chối) thì không còn tính, bạn nộp tiếp được. Mỗi doanh nghiệp chỉ nộp một lần trong một đợt.
## Khi nào không nộp được [#khi-nào-không-nộp-được]
- Chưa có CV, số điện thoại hoặc mã sinh viên.
- Không có đợt đang mở, hoặc bạn chưa có tên trong đợt.
- Bạn đã có nơi thực tập.
- Số hồ sơ đang chờ của bạn đã đạt giới hạn.
- Bạn đã nộp vào doanh nghiệp này trong đợt.
- Doanh nghiệp đã đủ số hồ sơ chờ hoặc đủ số sinh viên tiếp nhận, đã đóng nhận hồ sơ, hoặc đang bị nhà trường tạm ẩn.
Hệ thống không gửi email khi bạn vừa nộp hồ sơ; theo dõi ở trang **Thông tin cá nhân**.
# Tìm doanh nghiệp (/en/thuc-tap-doanh-nghiep/nguoi-dung/sinh-vien/tim-doanh-nghiep)
Trang **Khám phá** (trang chủ của cổng) liệt kê các doanh nghiệp tham gia đợt đang mở.
- **Tìm** theo tên doanh nghiệp.
- **Lọc**: **Tất cả**, **Đang tuyển**, **Nhà tài trợ**, **Đã đủ**.
- **Sắp xếp**: ngẫu nhiên, theo tên, hoặc còn nhiều chỉ tiêu nhất.
- **Đổi cách xem**: lưới thẻ, danh sách, hoặc tường logo.
Mở một doanh nghiệp để xem giới thiệu chương trình thực tập, số chỉ tiêu còn lại và chính sách thực tập. Địa chỉ, thông tin liên hệ và tệp chính sách chỉ hiện khi đã đăng nhập.
Doanh nghiệp **Đã đủ** không nhận thêm hồ sơ; hãy chọn nơi khác.