This commit is contained in:
zhouwentao 2026-02-01 13:48:16 +08:00
parent 1b7baf85df
commit d5cffd2a76
6 changed files with 778 additions and 54 deletions

View File

@ -0,0 +1,515 @@
# 用户志愿控制器 API 接口文档
## 概述
用户志愿控制器 (UserVolunteerController) 提供了志愿管理的相关接口,包括志愿保存、详情查询、名称修改、列表查询、删除和切换功能。
**基础路径**: `/api/user/volunteer`
**认证方式**: 需要登录认证,通过 JWT Token 进行身份验证
---
## 1. 保存志愿明细
保存用户选择的志愿专业列表到当前激活的志愿表中。
**请求**
- **方法**: `POST`
- **路径**: `/api/user/volunteer/save`
- **Content-Type**: `application/json`
**请求参数**
| 参数名 | 类型 | 必填 | 说明 |
|--------|--------|------|----------------------------------------|
| keys | string[] | 是 | 志愿专业Key列表格式`学校代码_专业代码_招生代码` |
**请求示例**
```json
[
"10001_1001_001",
"10002_2001_002",
"10003_3001_003"
]
```
**响应**
| 字段名 | 类型 | 说明 |
|----------|--------|----------|
| code | int | 状态码 |
| message | string | 响应消息 |
| data | string | 响应数据 |
**成功响应示例**
```json
{
"code": 200,
"message": "success",
"data": "保存成功"
}
```
**错误响应示例**
```json
{
"code": 500,
"message": "未找到计算表名",
"data": null
}
```
**错误码说明**
| 错误码 | 说明 |
|--------|----------------------------|
| 500 | 获取用户成绩信息失败 |
| 500 | 未找到计算表名 |
| 500 | 查找志愿表失败 |
| 500 | 请先创建志愿表 |
| 500 | 查找专业信息失败 |
| 500 | 删除旧数据失败 |
| 500 | 保存失败 |
**业务逻辑**
1. 对传入的keys进行去重处理
2. 获取当前登录用户的激活成绩信息
3. 查找当前激活的志愿表
4. 根据keys查找对应的专业信息
5. 构建志愿记录列表,保持提交顺序
6. 先删除旧的志愿记录,再批量插入新记录
---
## 2. 获取当前志愿单详情
获取当前激活志愿单的详细信息,包括志愿记录按批次分组展示。
**请求**
- **方法**: `GET`
- **路径**: `/api/user/volunteer/detail`
**请求参数**
**响应**
| 字段名 | 类型 | 说明 |
|----------|--------|--------------------|
| code | int | 状态码 |
| message | string | 响应消息 |
| data | object | 志愿详情数据 |
**data 数据结构**
| 字段名 | 类型 | 说明 |
|------------|--------|--------------------------|
| volunteer | object | 志愿单基本信息 |
| items | object | 按批次分组的志愿明细 |
**items 数据结构**
| 批次 | 类型 | 说明 |
|--------|------|------|
| 提前批 | array | 提前批志愿记录列表 |
| 本科批 | array | 本科批志愿记录列表 |
| 专科批 | array | 专科批志愿记录列表 |
**志愿记录项数据结构**
| 字段名 | 类型 | 说明 |
|------------------------|---------|--------------------------------|
| volunteerID | string | 志愿单ID |
| schoolCode | string | 学校代码 |
| majorCode | string | 专业代码 |
| enrollmentCode | string | 招生代码 |
| indexs | int | 志愿序号 |
| batch | string | 批次 |
| enrollProbability | float64 | 录取概率 |
| studentConvertedScore | float64 | 学生折合分 |
| calculationMajorID | string | 计算专业ID |
| schoolName | string | 学校名称 |
| majorName | string | 专业名称 |
| planNum | int | 计划人数 |
| tuition | string | 学费 |
| province | string | 省份 |
| schoolNature | string | 院校性质 |
| institutionType | string | 院校类型 |
| majorDetail | string | 专业详情 |
**成功响应示例**
```json
{
"code": 200,
"message": "success",
"data": {
"volunteer": {
"id": "123456789",
"volunteerName": "我的志愿表",
"scoreId": "987654321",
"createType": 1,
"state": 1
},
"items": {
"提前批": [
{
"volunteerID": "123456789",
"schoolCode": "10001",
"majorCode": "1001",
"enrollmentCode": "001",
"indexs": 1,
"batch": "本科提前批",
"schoolName": "某某大学",
"majorName": "美术学",
"planNum": 30,
"tuition": "8000元/年",
"enrollProbability": 85.5
}
],
"本科批": [],
"专科批": []
}
}
}
```
**错误码说明**
| 错误码 | 说明 |
|--------|------------------------|
| 500 | 获取用户成绩信息失败 |
| 500 | 查找志愿表失败 |
| 500 | 查找志愿明细失败 |
**批次分类规则**
- **提前批**: `提前批`、`本科提前批`
- **专科批**: `高职高专`、`专科批`
- **本科批**: 其他所有批次
---
## 3. 编辑志愿单名称
修改志愿单的名称。
**请求**
- **方法**: `PUT`
- **路径**: `/api/user/volunteer/updateName`
**请求参数**
| 参数名 | 类型 | 必填 | 位置 | 说明 |
|--------|--------|------|--------|------------|
| id | string | 是 | query | 志愿单ID |
| name | string | 是 | query | 志愿单名称 |
**请求示例**
```
PUT /api/user/volunteer/updateName?id=123456789&name=我的新志愿表
```
**响应**
| 字段名 | 类型 | 说明 |
|----------|--------|----------|
| code | int | 状态码 |
| message | string | 响应消息 |
| data | string | 响应数据 |
**成功响应示例**
```json
{
"code": 200,
"message": "success",
"data": "更新成功"
}
```
**错误响应示例**
```json
{
"code": 400,
"message": "参数错误",
"data": null
}
```
**错误码说明**
| 错误码 | 说明 |
|--------|----------------|
| 400 | 参数错误 |
| 500 | 更新失败 |
---
## 4. 获取当前用户志愿单列表
分页查询当前用户的志愿单列表。
**请求**
- **方法**: `GET`
- **路径**: `/api/user/volunteer/list`
**请求参数**
| 参数名 | 类型 | 必填 | 位置 | 默认值 | 说明 |
|--------|------|------|-------|--------|--------|
| page | int | 否 | query | 1 | 页码 |
| size | int | 否 | query | 10 | 每页数量 |
**请求示例**
```
GET /api/user/volunteer/list?page=1&size=10
```
**响应**
| 字段名 | 类型 | 说明 |
|----------|--------|----------|
| code | int | 状态码 |
| message | string | 响应消息 |
| data | object | 响应数据 |
**data 数据结构**
| 字段名 | 类型 | 说明 |
|--------|--------|--------------|
| items | array | 志愿单列表 |
| total | int64 | 总数量 |
**志愿单数据结构**
| 字段名 | 类型 | 说明 |
|----------------|---------|------------------------------|
| id | string | 志愿单ID |
| volunteerName | string | 志愿单名称 |
| scoreId | string | 关联成绩ID |
| createType | int | 生成类型 (1.手动, 2.智能) |
| state | string | 状态 (0.未使用, 1.使用中, 2.历史) |
| createBy | string | 创建人 |
| createTime | string | 创建时间 |
| updateBy | string | 更新人 |
| updateTime | string | 更新时间 |
**成功响应示例**
```json
{
"code": 200,
"message": "success",
"data": {
"items": [
{
"id": "123456789",
"volunteerName": "我的志愿表",
"scoreId": "987654321",
"createType": 1,
"state": "1",
"createBy": "user001",
"createTime": "2026-01-31T10:00:00Z"
}
],
"total": 1
}
}
```
**错误码说明**
| 错误码 | 说明 |
|--------|----------------|
| 500 | 查询失败 |
---
## 5. 删除志愿单
删除指定的志愿单。
**请求**
- **方法**: `DELETE`
- **路径**: `/api/user/volunteer/delete`
**请求参数**
| 参数名 | 类型 | 必填 | 位置 | 说明 |
|--------|--------|------|-------|----------|
| id | string | 是 | query | 志愿单ID |
**请求示例**
```
DELETE /api/user/volunteer/delete?id=123456789
```
**响应**
| 字段名 | 类型 | 说明 |
|----------|--------|----------|
| code | int | 状态码 |
| message | string | 响应消息 |
| data | string | 响应数据 |
**成功响应示例**
```json
{
"code": 200,
"message": "success",
"data": "删除成功"
}
```
**错误响应示例**
```json
{
"code": 400,
"message": "参数错误",
"data": null
}
```
**错误码说明**
| 错误码 | 说明 |
|--------|----------------|
| 400 | 参数错误 |
| 500 | 删除失败 |
---
## 6. 切换当前志愿单
切换当前激活的志愿单。
**请求**
- **方法**: `POST`
- **路径**: `/api/user/volunteer/switch`
**请求参数**
| 参数名 | 类型 | 必填 | 位置 | 说明 |
|--------|--------|------|-------|----------|
| id | string | 是 | query | 志愿单ID |
**请求示例**
```
POST /api/user/volunteer/switch?id=123456789
```
**响应**
| 字段名 | 类型 | 说明 |
|----------|--------|----------|
| code | int | 状态码 |
| message | string | 响应消息 |
| data | string | 响应数据 |
**成功响应示例**
```json
{
"code": 200,
"message": "success",
"data": "切换成功"
}
```
**特殊响应示例(已是当前志愿单)**
```json
{
"code": 200,
"message": "success",
"data": "已是当前志愿单,忽略切换"
}
```
**错误码说明**
| 错误码 | 说明 |
|--------|----------------------------|
| 400 | 参数错误 |
| 500 | 获取用户成绩信息失败 |
| 500 | 切换失败 |
**业务逻辑**
1. 检查当前是否已是该志愿单,如果是则忽略切换
2. 执行志愿单切换操作
3. Redis 缓存同步(由 Service 层处理)
---
## 通用响应格式说明
### 成功响应
```json
{
"code": 200,
"message": "success",
"data": {}
}
```
### 错误响应
```json
{
"code": 错误码,
"message": "错误信息",
"data": null
}
```
### 常见错误码
| 错误码 | 说明 |
|--------|--------------------|
| 200 | 成功 |
| 400 | 请求参数错误 |
| 500 | 服务器内部错误 |
---
## 认证说明
所有接口都需要在请求头中携带 JWT Token
```
Authorization: Bearer {token}
```
Token 从登录接口获取有效期24小时。
---
## 注意事项
1. **保存志愿明细**: 每次保存会先删除当前志愿表中的所有记录,再插入新记录
2. **批次分类**: 志愿详情按 提前批、本科批、专科批 三大类分组展示
3. **数据去重**: 保存志愿时会自动去重,避免重复添加相同专业
4. **顺序保持**: 志愿序号按照提交的 keys 顺序依次递增
5. **权限控制**: 所有操作都基于当前登录用户的身份,只能操作自己的志愿数据

View File

@ -3,7 +3,7 @@ import type { FilterState } from '~/components/FilterBar.vue'
import { onMounted, ref, watch, computed } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { getUserMajorList, type MajorItem } from '~/service/api/major'
import { saveVolunteer, getVolunteerDetail, type VolunteerItem, type VolunteerInfo } from '~/service/api/volunteer'
import { saveVolunteer, getVolunteerDetail, updateVolunteerName, getVolunteerList, deleteVolunteer, switchVolunteer, type VolunteerItem, type VolunteerInfo, type VolunteerPlanItem } from '~/service/api/volunteer'
// --- ---
type TabKey = 'all' | 'hard' | 'risky' | 'safe' | 'stable' | '本科' | '专科' | '985/211/双一流' | '公办本科' | '民办本科'
@ -32,34 +32,12 @@ interface MajorDetail {
// --- ---
const showSwitchModal = ref(false) //
const activePlanId = ref('2025121426') // ID
const showEditNameModal = ref(false) //
const editingPlanName = ref('') //
const isUpdatingName = ref(false) // Loading
const activePlanId = ref('') // ID
// ()
const volunteerPlans = ref([
{
id: '2025121426',
name: '志愿2025121426',
tag: '手动', //
province: '北京',
artType: '美术与设计类',
cultureScore: 450,
cultureSubjects: '物化生',
artScore: 250,
updateTime: '2025-12-14 10:33:46',
status: '有效',
},
{
id: '2025121427',
name: '志愿2025121427',
tag: '智能',
province: '湖北',
artType: '音乐类',
cultureScore: 480,
cultureSubjects: '历史',
artScore: 240,
updateTime: '2025-12-13 14:20:00',
status: '有效',
},
])
const volunteerPlans = ref([])
const activePanel = ref<PanelType>('market') //
const myVolunteers = ref<VolunteerItem[]>([])
@ -560,30 +538,133 @@ function handleCreatePlan() {
}
function handleEditPlan() {
console.warn('点击修改方案信息')
// /...
if (!currentVolunteerInfo.value?.volunteerName) {
// @ts-ignore
window.$message?.warning?.('当前志愿单信息为空')
return
}
editingPlanName.value = currentVolunteerInfo.value.volunteerName
showEditNameModal.value = true
}
function handleSwitchPlan() {
/**
* 保存志愿单名称
*/
async function saveVolunteerName() {
if (!currentVolunteerInfo.value?.id || !editingPlanName.value.trim()) {
// @ts-ignore
window.$message?.warning?.('志愿单名称不能为空')
return
}
isUpdatingName.value = true
try {
await updateVolunteerName(currentVolunteerInfo.value.id, editingPlanName.value.trim())
// @ts-ignore
window.$message?.success?.('名称修改成功')
//
if (currentVolunteerInfo.value) {
currentVolunteerInfo.value.volunteerName = editingPlanName.value.trim()
}
showEditNameModal.value = false
} catch (error) {
console.error('修改志愿单名称失败:', error)
// @ts-ignore
window.$message?.error?.('修改失败,请重试')
} finally {
isUpdatingName.value = false
}
}
/**
* 取消编辑名称
*/
function cancelEditName() {
showEditNameModal.value = false
editingPlanName.value = ''
}
/**
* 打开切换方案弹框并加载方案列表
*/
async function handleSwitchPlan() {
showSwitchModal.value = true
try {
const res = await getVolunteerList(1, 100)
if (res && res.items) {
// API
volunteerPlans.value = res.items.map(item => ({
id: item.id,
name: item.volunteerName,
tag: item.createType === '1' ? '手动' : '智能',
province: '河南', // API 使
artType: '美术与设计类',
culturalScore: item.culturalScore,
professionalScore: item.professionalScore,
cultureSubjects: '-',
updateTime: item.updateTime,
status: item.state === '1' ? '使用中' : (item.state === '0' ? '未使用' : '历史'),
}))
// ID
if (currentVolunteerInfo.value?.id) {
activePlanId.value = currentVolunteerInfo.value.id
}
}
} catch (error) {
console.error('获取志愿单列表失败:', error)
// @ts-ignore
window.$message?.error?.('获取志愿单列表失败')
}
}
function handleExportPlan() {
console.warn('点击导出当前方案')
}
//
function switchActivePlan(planId: string) {
/**
* 切换到指定方案
*/
async function switchActivePlan(planId: string) {
try {
await switchVolunteer(planId)
activePlanId.value = planId
showSwitchModal.value = false
console.warn('切换到了方案:', planId)
// myVolunteers ...
// isLoading.value = true ...
// @ts-ignore
window.$message?.success?.('切换志愿单成功')
//
await fetchVolunteerDetail()
} catch (error) {
console.error('切换志愿单失败:', error)
// @ts-ignore
window.$message?.error?.('切换志愿单失败')
}
}
function deletePlan(planId: string) {
console.warn('删除方案:', planId)
// API...
/**
* 删除志愿单
*/
async function deletePlan(planId: string) {
const confirmDelete = window.confirm('确定要删除这个志愿单吗?删除后无法恢复。')
if (!confirmDelete) return
try {
await deleteVolunteer(planId)
// @ts-ignore
window.$message?.success?.('删除成功')
//
const index = volunteerPlans.value.findIndex(p => p.id === planId)
if (index > -1) {
volunteerPlans.value.splice(index, 1)
}
//
if (activePlanId.value === planId) {
await fetchVolunteerDetail()
}
} catch (error) {
console.error('删除志愿单失败:', error)
// @ts-ignore
window.$message?.error?.('删除失败')
}
}
</script>
@ -1478,8 +1559,7 @@ function deletePlan(planId: string) {
class="appearance-none border border-slate-300 dark:border-slate-700 rounded bg-white dark:bg-slate-800 py-1.5 pl-3 pr-8 text-sm text-slate-700 dark:text-slate-300 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
>
<option>请选择省份</option>
<option>北京</option>
<option>湖北</option>
<option>河南</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-slate-500 dark:text-slate-600">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -1540,12 +1620,12 @@ function deletePlan(planId: string) {
{{ plan.artType }}
</td>
<td class="border-r border-slate-100 dark:border-slate-800 px-4 py-3 text-center">
<span class="text-slate-800 dark:text-slate-200 font-medium">{{ plan.cultureScore }}</span>
<span class="ml-1 text-xs text-slate-400 dark:text-slate-500">{{ plan.cultureSubjects }}</span>
<span class="text-slate-800 dark:text-slate-200 font-medium">{{ plan.culturalScore }}</span>
<!-- <span class="ml-1 text-xs text-slate-400 dark:text-slate-500">{{ plan.professionalScore }}</span> -->
</td>
<td class="border-r border-slate-100 dark:border-slate-800 px-4 py-3 text-center text-slate-800 dark:text-slate-200 font-medium">
{{
plan.artScore }}
plan.professionalScore }}
</td>
<td class="border-r border-slate-100 dark:border-slate-800 px-4 py-3 text-center text-xs text-slate-500 dark:text-slate-500 font-mono">
{{
@ -1584,6 +1664,72 @@ function deletePlan(planId: string) {
</div>
</div>
</div>
<!-- =========================================================
编辑名称弹框 (新增)
========================================================== -->
<div
v-if="showEditNameModal"
class="fixed inset-0 z-[110] flex items-center justify-center bg-black/50 backdrop-blur-sm"
@click.self="cancelEditName"
>
<div
class="w-[400px] flex flex-col animate-fade-in-up overflow-hidden rounded-lg bg-white dark:bg-slate-900 shadow-2xl border border-transparent dark:border-slate-800"
>
<!-- Header -->
<div class="flex items-center justify-between border-b border-slate-200 dark:border-slate-800 px-6 py-4">
<h3 class="text-lg text-slate-800 dark:text-slate-100 font-bold">
修改志愿单名称
</h3>
<button class="text-slate-400 dark:text-slate-500 hover:text-slate-600 dark:hover:text-slate-300" @click="cancelEditName">
<svg
xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Content -->
<div class="p-6">
<div class="mb-4">
<label class="block mb-2 text-sm font-medium text-slate-700 dark:text-slate-300">
志愿单名称
</label>
<input
v-model="editingPlanName"
type="text"
class="w-full border border-slate-300 dark:border-slate-700 rounded-lg bg-white dark:bg-slate-800 px-4 py-2.5 text-sm text-slate-900 dark:text-slate-100 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50"
placeholder="请输入志愿单名称"
maxlength="50"
@keyup.enter="saveVolunteerName"
/>
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
最多输入 50 个字符
</p>
</div>
</div>
<!-- Footer -->
<div class="flex items-center justify-end gap-3 border-t border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-800/50 px-6 py-4">
<button
class="rounded-lg px-4 py-2 text-sm text-slate-600 dark:text-slate-400 transition-colors hover:bg-slate-100 dark:hover:bg-slate-800"
@click="cancelEditName"
>
取消
</button>
<button
class="rounded-lg bg-blue-600 px-4 py-2 text-sm text-white font-medium transition-colors hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="!editingPlanName.trim() || isUpdatingName"
@click="saveVolunteerName"
>
<span v-if="isUpdatingName" class="mr-1 inline-block h-3 w-3 animate-spin border-2 border-white/30 border-t-white rounded-full"></span>
保存
</button>
</div>
</div>
</div>
</Teleport>
</div>
</template>

View File

@ -28,12 +28,29 @@ export interface VolunteerDetailResponse {
items: Record<string, VolunteerItem[]>
}
export interface VolunteerPlanItem {
id: string
volunteerName: string
scoreId: string
createType: string
state: string
createBy: string
createTime: string
updateBy: string
updateTime: string
}
export interface VolunteerListResponse {
items: VolunteerPlanItem[]
total: number
}
/**
*
* @param data schoolCode_majorCode_enrollmentCode
*/
export function saveVolunteer(data: string[]) {
return request.post('/user/volunteer/save', data)
return request.post('/user/volunteer/save', {keys:data})
}
/**
@ -42,3 +59,37 @@ export function saveVolunteer(data: string[]) {
export function getVolunteerDetail() {
return request.get<VolunteerDetailResponse>('/user/volunteer/detail')
}
/**
*
* @param id ID
* @param name
*/
export function updateVolunteerName(id: string, name: string) {
return request.put('/user/volunteer/updateName', null, { params: { id, name } })
}
/**
*
* @param page
* @param size
*/
export function getVolunteerList(page: number = 1, size: number = 10) {
return request.get<VolunteerListResponse>('/user/volunteer/list', { params: { page, size } })
}
/**
*
* @param id ID
*/
export function deleteVolunteer(id: string) {
return request.delete('/user/volunteer/delete', { params: { id } })
}
/**
*
* @param id ID
*/
export function switchVolunteer(id: string) {
return request.post('/user/volunteer/switch', null, { params: { id } })
}

View File

@ -141,6 +141,14 @@ class Request {
post<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
return this.instance.post(url, data, config)
}
put<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
return this.instance.put(url, data, config)
}
delete<T = any>(url: string, config?: RequestConfig): Promise<T> {
return this.instance.delete(url, config)
}
}
export default new Request({

View File

@ -22,9 +22,13 @@ export interface DictType {
const staticDicts: DictType = {
// 专业类别
professionalCategory: [
{ label: '理工类', value: 'science', color: '#108ee9' },
{ label: '文史类', value: 'liberal_arts', color: '#2db7f5' },
{ label: '艺术类', value: 'art', color: '#87d068' },
{ label: '美术与设计类', value: 'science', color: '#108ee9' },
{ label: '播音与主持类', value: 'liberal_arts', color: '#2db7f5' },
{ label: '表演类', value: 'art', color: '#87d068' },
{ label: '音乐类', value: 'sports', color: '#ff5500' },
{ label: '舞蹈类', value: 'sports', color: '#ff5500' },
{ label: '书法类', value: 'sports', color: '#ff5500' },
{ label: '戏曲类', value: 'sports', color: '#ff5500' },
{ label: '体育类', value: 'sports', color: '#ff5500' },
],
@ -73,9 +77,9 @@ const staticDicts: DictType = {
// 科目列表
subjectList: [
{ label: '语文', value: 'chinese', color: '#108ee9' },
{ label: '数学', value: 'mathematics', color: '#2db7f5' },
{ label: '英语', value: 'english', color: '#87d068' },
// { label: '语文', value: 'chinese', color: '#108ee9' },
// { label: '数学', value: 'mathematics', color: '#2db7f5' },
// { label: '英语', value: 'english', color: '#87d068' },
{ label: '物理', value: 'physics', color: '#ff5500' },
{ label: '化学', value: 'chemistry', color: '#f5222d' },
{ label: '生物', value: 'biology', color: '#fa8c16' },

View File

@ -36,7 +36,7 @@ const Message = (options: MessageOptions) => {
if (!messageContainer) {
messageContainer = document.createElement('div')
// Common classes
let classes = `w-message-container ${containerClass} fixed z-50 flex pointer-events-none transition-all duration-300`
let classes = `w-message-container ${containerClass} fixed z-[9999] flex pointer-events-none transition-all duration-300`
// Position specific classes
classes += ` ${positionClasses[position]}`