引入openspec管理
This commit is contained in:
127
openspec/specs/api_response_schema.yaml
Normal file
127
openspec/specs/api_response_schema.yaml
Normal file
@@ -0,0 +1,127 @@
|
||||
version: 1
|
||||
id: api_response_schema
|
||||
title: Unified API Response Envelope
|
||||
sources:
|
||||
project_md:
|
||||
- path: openspec/project.md
|
||||
lines: "16-19"
|
||||
middleware:
|
||||
- path: api/middlewares/index.ts
|
||||
lines: "76-94"
|
||||
legacy_app:
|
||||
- path: api/app.ts
|
||||
lines: "45-66"
|
||||
|
||||
json_schema_draft: "2020-12"
|
||||
schemas:
|
||||
Pagination:
|
||||
type: object
|
||||
required:
|
||||
- page
|
||||
- limit
|
||||
- total
|
||||
- pages
|
||||
properties:
|
||||
page:
|
||||
type: integer
|
||||
minimum: 1
|
||||
limit:
|
||||
type: integer
|
||||
minimum: 1
|
||||
total:
|
||||
type: integer
|
||||
minimum: 0
|
||||
pages:
|
||||
type: integer
|
||||
minimum: 0
|
||||
additionalProperties: true
|
||||
|
||||
ApiResponse:
|
||||
type: object
|
||||
required:
|
||||
- success
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
|
||||
message:
|
||||
type: string
|
||||
|
||||
data:
|
||||
description: |
|
||||
业务返回体,形态取决于具体接口:
|
||||
- 单对象 / 列表 / 聚合对象 / 统计对象
|
||||
nullable: true
|
||||
oneOf:
|
||||
- type: object
|
||||
- type: array
|
||||
- type: string
|
||||
- type: number
|
||||
- type: boolean
|
||||
|
||||
pagination:
|
||||
$ref: "#/schemas/Pagination"
|
||||
|
||||
errors:
|
||||
description: |
|
||||
业务校验错误明细(当前仅在部分接口中使用,如用户数据校验)。
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
error:
|
||||
description: |
|
||||
兼容字段:`api/app.ts` 的错误/404 处理使用 `error` 字段。
|
||||
type: string
|
||||
|
||||
additionalProperties: true
|
||||
|
||||
constraints:
|
||||
- id: envelope_is_best_effort
|
||||
status: implemented
|
||||
evidence:
|
||||
- api/middlewares/index.ts:76
|
||||
details: |
|
||||
响应格式化中间件仅在返回体不含 `success` 字段时进行包装:
|
||||
- 包装为 `{ success: true, data: <original> }`
|
||||
- 控制器自行返回 `{ success: boolean, ... }` 时不二次包装
|
||||
|
||||
- id: legacy_error_shape_exists
|
||||
status: implemented
|
||||
evidence:
|
||||
- api/app.ts:45
|
||||
- api/app.ts:58
|
||||
details: |
|
||||
`api/app.ts` 的错误与 404 返回使用 `{ success: false, error: string }`,
|
||||
与 project.md 中的 `{ success, message, data }` 不完全一致。
|
||||
|
||||
examples:
|
||||
success_with_data:
|
||||
success: true
|
||||
data:
|
||||
id: "uuid"
|
||||
success_with_message_and_data:
|
||||
success: true
|
||||
message: "登录成功"
|
||||
data:
|
||||
token: "admin-token"
|
||||
success_with_pagination:
|
||||
success: true
|
||||
data: []
|
||||
pagination:
|
||||
page: 1
|
||||
limit: 10
|
||||
total: 0
|
||||
pages: 0
|
||||
error_with_message:
|
||||
success: false
|
||||
message: "参数不完整"
|
||||
error_with_errors_array:
|
||||
success: false
|
||||
message: "数据验证失败"
|
||||
errors:
|
||||
- "手机号格式不正确"
|
||||
legacy_error:
|
||||
success: false
|
||||
error: "API not found"
|
||||
|
||||
103
openspec/specs/auth_rules.yaml
Normal file
103
openspec/specs/auth_rules.yaml
Normal file
@@ -0,0 +1,103 @@
|
||||
version: 1
|
||||
id: auth_rules
|
||||
title: Authorization Rules
|
||||
sources:
|
||||
primary_server:
|
||||
path: api/server.ts
|
||||
middleware:
|
||||
path: api/middlewares/index.ts
|
||||
project_md:
|
||||
path: openspec/project.md
|
||||
lines: "65-69"
|
||||
legacy_app:
|
||||
path: api/app.ts
|
||||
|
||||
auth_model:
|
||||
current_state: simplified_admin_auth
|
||||
description: |
|
||||
管理端接口普遍加了 `adminAuth` 中间件,但 `adminAuth` 当前为放行实现,
|
||||
不校验 token、不区分用户身份。
|
||||
|
||||
roles:
|
||||
- id: public
|
||||
description: "无需鉴权的访问方(当前系统中也包含前台用户行为)。"
|
||||
- id: admin
|
||||
description: "管理端访问方(当前仅通过前端保存 token 来区分 UI 状态)。"
|
||||
|
||||
mechanisms:
|
||||
admin_login:
|
||||
endpoint: "POST /api/admin/login"
|
||||
returns:
|
||||
token:
|
||||
type: string
|
||||
fixed_value: "admin-token"
|
||||
enforcement:
|
||||
middleware: adminAuth
|
||||
effective: allow_all
|
||||
evidence:
|
||||
- api/controllers/adminController.ts:1
|
||||
- api/middlewares/index.ts:57
|
||||
|
||||
middlewares:
|
||||
adminAuth:
|
||||
file: api/middlewares/index.ts
|
||||
behavior: allow_all
|
||||
notes: "当前实现为 next() 直接放行;生产环境需替换为真实鉴权。"
|
||||
|
||||
route_policies:
|
||||
- id: admin_namespace
|
||||
match:
|
||||
path_prefix: /api/admin/
|
||||
intended_guard: adminAuth
|
||||
effective_guard: none
|
||||
notes: "server.ts 中几乎所有 /api/admin/* 路由均挂载 adminAuth。"
|
||||
|
||||
- id: admin_login
|
||||
match:
|
||||
path: /api/admin/login
|
||||
methods: [POST]
|
||||
intended_guard: none
|
||||
effective_guard: none
|
||||
|
||||
- id: admin_protected_non_admin_prefix
|
||||
match:
|
||||
routes:
|
||||
- path: /api/questions
|
||||
methods: [POST]
|
||||
- path: /api/questions/:id
|
||||
methods: [PUT, DELETE]
|
||||
- path: /api/questions/import
|
||||
methods: [POST]
|
||||
- path: /api/questions/export
|
||||
methods: [GET]
|
||||
- path: /questions/export
|
||||
methods: [GET]
|
||||
- path: /api/quiz/records
|
||||
methods: [GET]
|
||||
intended_guard: adminAuth
|
||||
effective_guard: none
|
||||
|
||||
legacy_routes:
|
||||
- id: auth_demo
|
||||
mount_path: /api/auth
|
||||
file: api/routes/auth.ts
|
||||
status: not_implemented
|
||||
notes: "register/login/logout 均为 TODO,占位路由。"
|
||||
|
||||
constraints:
|
||||
- id: admin_auth_is_not_enforced
|
||||
severity: high
|
||||
evidence:
|
||||
- api/middlewares/index.ts:57
|
||||
- openspec/project.md:68
|
||||
details: |
|
||||
`adminAuth` 当前不校验任何凭证,导致管理接口实际可被任何请求方访问。
|
||||
|
||||
- id: admin_token_is_fixed
|
||||
severity: medium
|
||||
evidence:
|
||||
- api/controllers/adminController.ts:1
|
||||
- openspec/project.md:68
|
||||
details: |
|
||||
`/api/admin/login` 返回固定 token `admin-token`,不具备会话隔离与过期能力。
|
||||
|
||||
425
openspec/specs/database_schema.yaml
Normal file
425
openspec/specs/database_schema.yaml
Normal file
@@ -0,0 +1,425 @@
|
||||
version: 1
|
||||
id: database_schema
|
||||
title: SQLite Database Schema
|
||||
sources:
|
||||
init_sql:
|
||||
path: api/database/init.sql
|
||||
init_code:
|
||||
path: api/database/index.ts
|
||||
notes: "仅在 users 表不存在时执行 init.sql"
|
||||
models_dir:
|
||||
path: api/models
|
||||
|
||||
database:
|
||||
engine: sqlite3
|
||||
file_path:
|
||||
env: DB_PATH
|
||||
default: data/survey.db
|
||||
pragmas:
|
||||
foreign_keys: true
|
||||
evidence:
|
||||
- api/database/index.ts:36
|
||||
|
||||
tables:
|
||||
users:
|
||||
source:
|
||||
init_sql_lines: "1-12"
|
||||
model: api/models/user.ts
|
||||
columns:
|
||||
id:
|
||||
type: TEXT
|
||||
primary_key: true
|
||||
nullable: false
|
||||
name:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
checks:
|
||||
- "length(name) >= 2 AND length(name) <= 20"
|
||||
phone:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
unique: true
|
||||
checks:
|
||||
- "length(phone) = 11"
|
||||
- "phone LIKE '1%'"
|
||||
- "substr(phone, 2, 1) BETWEEN '3' AND '9'"
|
||||
password:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
default: "''"
|
||||
created_at:
|
||||
type: DATETIME
|
||||
nullable: false
|
||||
default: CURRENT_TIMESTAMP
|
||||
indexes:
|
||||
- name: idx_users_phone
|
||||
columns: [phone]
|
||||
- name: idx_users_created_at
|
||||
columns: [created_at]
|
||||
|
||||
questions:
|
||||
source:
|
||||
init_sql_lines: "14-29"
|
||||
model: api/models/question.ts
|
||||
columns:
|
||||
id:
|
||||
type: TEXT
|
||||
primary_key: true
|
||||
nullable: false
|
||||
content:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
type:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
checks:
|
||||
- "type IN ('single', 'multiple', 'judgment', 'text')"
|
||||
options:
|
||||
type: TEXT
|
||||
nullable: true
|
||||
notes: "JSON 字符串,存储选项数组"
|
||||
answer:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
notes: "多选题答案可能为 JSON 字符串或可解析为数组的字符串"
|
||||
score:
|
||||
type: INTEGER
|
||||
nullable: false
|
||||
checks:
|
||||
- "score > 0"
|
||||
category:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
default: "'通用'"
|
||||
created_at:
|
||||
type: DATETIME
|
||||
nullable: false
|
||||
default: CURRENT_TIMESTAMP
|
||||
indexes:
|
||||
- name: idx_questions_type
|
||||
columns: [type]
|
||||
- name: idx_questions_score
|
||||
columns: [score]
|
||||
- name: idx_questions_category
|
||||
columns: [category]
|
||||
|
||||
question_categories:
|
||||
source:
|
||||
init_sql_lines: "31-38"
|
||||
model: api/models/questionCategory.ts
|
||||
columns:
|
||||
id:
|
||||
type: TEXT
|
||||
primary_key: true
|
||||
nullable: false
|
||||
name:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
unique: true
|
||||
created_at:
|
||||
type: DATETIME
|
||||
nullable: false
|
||||
default: CURRENT_TIMESTAMP
|
||||
seed:
|
||||
- id: default
|
||||
name: 通用
|
||||
|
||||
exam_subjects:
|
||||
source:
|
||||
init_sql_lines: "40-50"
|
||||
model: api/models/examSubject.ts
|
||||
columns:
|
||||
id:
|
||||
type: TEXT
|
||||
primary_key: true
|
||||
nullable: false
|
||||
name:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
unique: true
|
||||
type_ratios:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
notes: "JSON 字符串"
|
||||
category_ratios:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
notes: "JSON 字符串"
|
||||
total_score:
|
||||
type: INTEGER
|
||||
nullable: false
|
||||
duration_minutes:
|
||||
type: INTEGER
|
||||
nullable: false
|
||||
default: 60
|
||||
created_at:
|
||||
type: DATETIME
|
||||
nullable: false
|
||||
default: CURRENT_TIMESTAMP
|
||||
updated_at:
|
||||
type: DATETIME
|
||||
nullable: false
|
||||
default: CURRENT_TIMESTAMP
|
||||
|
||||
exam_tasks:
|
||||
source:
|
||||
init_sql_lines: "52-63"
|
||||
model: api/models/examTask.ts
|
||||
columns:
|
||||
id:
|
||||
type: TEXT
|
||||
primary_key: true
|
||||
nullable: false
|
||||
name:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
subject_id:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
foreign_key:
|
||||
table: exam_subjects
|
||||
column: id
|
||||
start_at:
|
||||
type: DATETIME
|
||||
nullable: false
|
||||
end_at:
|
||||
type: DATETIME
|
||||
nullable: false
|
||||
selection_config:
|
||||
type: TEXT
|
||||
nullable: true
|
||||
notes: "JSON 字符串;模型在读写该列,但 init.sql 未包含该列"
|
||||
created_at:
|
||||
type: DATETIME
|
||||
nullable: false
|
||||
default: CURRENT_TIMESTAMP
|
||||
indexes:
|
||||
- name: idx_exam_tasks_subject_id
|
||||
columns: [subject_id]
|
||||
|
||||
exam_task_users:
|
||||
source:
|
||||
init_sql_lines: "65-77"
|
||||
model: api/models/examTask.ts
|
||||
columns:
|
||||
id:
|
||||
type: TEXT
|
||||
primary_key: true
|
||||
nullable: false
|
||||
task_id:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
foreign_key:
|
||||
table: exam_tasks
|
||||
column: id
|
||||
on_delete: CASCADE
|
||||
user_id:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
foreign_key:
|
||||
table: users
|
||||
column: id
|
||||
on_delete: CASCADE
|
||||
created_at:
|
||||
type: DATETIME
|
||||
nullable: false
|
||||
default: CURRENT_TIMESTAMP
|
||||
uniques:
|
||||
- columns: [task_id, user_id]
|
||||
indexes:
|
||||
- name: idx_exam_task_users_task_id
|
||||
columns: [task_id]
|
||||
- name: idx_exam_task_users_user_id
|
||||
columns: [user_id]
|
||||
|
||||
quiz_records:
|
||||
source:
|
||||
init_sql_lines: "79-98"
|
||||
model: api/models/quiz.ts
|
||||
columns:
|
||||
id:
|
||||
type: TEXT
|
||||
primary_key: true
|
||||
nullable: false
|
||||
user_id:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
foreign_key:
|
||||
table: users
|
||||
column: id
|
||||
subject_id:
|
||||
type: TEXT
|
||||
nullable: true
|
||||
foreign_key:
|
||||
table: exam_subjects
|
||||
column: id
|
||||
task_id:
|
||||
type: TEXT
|
||||
nullable: true
|
||||
foreign_key:
|
||||
table: exam_tasks
|
||||
column: id
|
||||
total_score:
|
||||
type: INTEGER
|
||||
nullable: false
|
||||
correct_count:
|
||||
type: INTEGER
|
||||
nullable: false
|
||||
total_count:
|
||||
type: INTEGER
|
||||
nullable: false
|
||||
created_at:
|
||||
type: DATETIME
|
||||
nullable: false
|
||||
default: CURRENT_TIMESTAMP
|
||||
indexes:
|
||||
- name: idx_quiz_records_user_id
|
||||
columns: [user_id]
|
||||
- name: idx_quiz_records_created_at
|
||||
columns: [created_at]
|
||||
- name: idx_quiz_records_subject_id
|
||||
columns: [subject_id]
|
||||
- name: idx_quiz_records_task_id
|
||||
columns: [task_id]
|
||||
|
||||
quiz_answers:
|
||||
source:
|
||||
init_sql_lines: "100-115"
|
||||
model: api/models/quiz.ts
|
||||
columns:
|
||||
id:
|
||||
type: TEXT
|
||||
primary_key: true
|
||||
nullable: false
|
||||
record_id:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
foreign_key:
|
||||
table: quiz_records
|
||||
column: id
|
||||
question_id:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
foreign_key:
|
||||
table: questions
|
||||
column: id
|
||||
user_answer:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
notes: "字符串或 JSON 字符串"
|
||||
score:
|
||||
type: INTEGER
|
||||
nullable: false
|
||||
is_correct:
|
||||
type: BOOLEAN
|
||||
nullable: false
|
||||
created_at:
|
||||
type: DATETIME
|
||||
nullable: false
|
||||
default: CURRENT_TIMESTAMP
|
||||
indexes:
|
||||
- name: idx_quiz_answers_record_id
|
||||
columns: [record_id]
|
||||
- name: idx_quiz_answers_question_id
|
||||
columns: [question_id]
|
||||
|
||||
system_configs:
|
||||
source:
|
||||
init_sql_lines: "117-131"
|
||||
model: api/models/systemConfig.ts
|
||||
columns:
|
||||
id:
|
||||
type: TEXT
|
||||
primary_key: true
|
||||
nullable: false
|
||||
config_type:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
unique: true
|
||||
config_value:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
notes: "JSON 字符串或普通字符串"
|
||||
updated_at:
|
||||
type: DATETIME
|
||||
nullable: false
|
||||
default: CURRENT_TIMESTAMP
|
||||
seed:
|
||||
- id: "1"
|
||||
config_type: quiz_config
|
||||
config_value: "{\"singleRatio\":40,\"multipleRatio\":30,\"judgmentRatio\":20,\"textRatio\":10,\"totalScore\":100}"
|
||||
- id: "2"
|
||||
config_type: admin_user
|
||||
config_value: "{\"username\":\"admin\",\"password\":\"admin123\"}"
|
||||
|
||||
user_groups:
|
||||
source:
|
||||
inferred_from_models:
|
||||
- api/models/userGroup.ts
|
||||
notes: "模型读写该表,但 init.sql 未包含该表"
|
||||
columns:
|
||||
id:
|
||||
type: TEXT
|
||||
primary_key: true
|
||||
nullable: false
|
||||
name:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
unique: true
|
||||
description:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
default: "''"
|
||||
is_system:
|
||||
type: INTEGER
|
||||
nullable: false
|
||||
default: 0
|
||||
notes: "0/1 标记系统内置用户组"
|
||||
created_at:
|
||||
type: DATETIME
|
||||
nullable: false
|
||||
default: CURRENT_TIMESTAMP
|
||||
|
||||
user_group_members:
|
||||
source:
|
||||
inferred_from_models:
|
||||
- api/models/userGroup.ts
|
||||
notes: "模型读写该表,但 init.sql 未包含该表"
|
||||
columns:
|
||||
group_id:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
foreign_key:
|
||||
table: user_groups
|
||||
column: id
|
||||
user_id:
|
||||
type: TEXT
|
||||
nullable: false
|
||||
foreign_key:
|
||||
table: users
|
||||
column: id
|
||||
created_at:
|
||||
type: DATETIME
|
||||
nullable: true
|
||||
notes: "模型查询中使用 m.created_at 排序,推断该列存在"
|
||||
uniques:
|
||||
- columns: [group_id, user_id]
|
||||
|
||||
constraints:
|
||||
- id: init_sql_missing_user_group_tables
|
||||
status: implemented_in_models_not_in_init_sql
|
||||
evidence:
|
||||
- api/models/userGroup.ts:1
|
||||
- api/database/init.sql:1
|
||||
details: |
|
||||
`init.sql` 未创建 `user_groups` / `user_group_members`,但后端模型与路由已使用该表。
|
||||
新初始化数据库时可能导致相关接口运行失败或功能不可用。
|
||||
|
||||
- id: init_sql_missing_exam_tasks_selection_config
|
||||
status: implemented_in_models_not_in_init_sql
|
||||
evidence:
|
||||
- api/models/examTask.ts:201
|
||||
- api/database/init.sql:52
|
||||
details: |
|
||||
`exam_tasks.selection_config` 在模型中读写,但 init.sql 的 exam_tasks 建表语句未包含该列。
|
||||
|
||||
102
openspec/specs/nfr.yaml
Normal file
102
openspec/specs/nfr.yaml
Normal file
@@ -0,0 +1,102 @@
|
||||
version: 1
|
||||
id: nfr
|
||||
title: Non-Functional Requirements
|
||||
sources:
|
||||
project_md:
|
||||
path: openspec/project.md
|
||||
lines: "67-70"
|
||||
middleware:
|
||||
- api/middlewares/index.ts
|
||||
database:
|
||||
- api/database/index.ts
|
||||
|
||||
security:
|
||||
password_handling:
|
||||
status: not_compliant
|
||||
current_state:
|
||||
storage: "明文存储在 users.password(SQLite)"
|
||||
transmission: "前后端请求体中直接传输 password 字段"
|
||||
evidence:
|
||||
- api/models/user.ts:18
|
||||
- api/controllers/userController.ts:81
|
||||
- openspec/project.md:69
|
||||
required_state:
|
||||
storage: "使用强哈希算法存储(例如 bcrypt/scrypt/argon2),不存明文"
|
||||
transmission: "避免回传密码;日志与导出不得包含敏感字段"
|
||||
constraints:
|
||||
- "当前实现未满足 required_state,属于待整改项"
|
||||
|
||||
admin_authentication:
|
||||
status: not_compliant
|
||||
current_state:
|
||||
admin_login_token: "固定值 admin-token"
|
||||
route_guard: "adminAuth 中间件放行"
|
||||
evidence:
|
||||
- api/controllers/adminController.ts:1
|
||||
- api/middlewares/index.ts:57
|
||||
- openspec/project.md:68
|
||||
required_state:
|
||||
token_validation: "生产环境需实现真实鉴权(例如 JWT 校验)并在前后端一致落地"
|
||||
constraints:
|
||||
- "当前管理接口在后端层面不具备访问控制"
|
||||
|
||||
logging_sensitivity:
|
||||
status: partial
|
||||
current_state:
|
||||
request_logging: "记录 method/path/statusCode/duration"
|
||||
evidence:
|
||||
- api/middlewares/index.ts:65
|
||||
constraints:
|
||||
- "应避免在日志中输出密码、token、导出数据等敏感信息(当前需持续自查)"
|
||||
|
||||
reliability:
|
||||
database_initialization:
|
||||
status: implemented
|
||||
behavior: "仅当 users 表不存在时执行 init.sql"
|
||||
evidence:
|
||||
- api/database/index.ts:109
|
||||
constraints:
|
||||
- "若数据库存在但缺少部分表/列(例如用户组、selection_config),当前不会自动迁移"
|
||||
|
||||
performance:
|
||||
limits:
|
||||
request_body_max_bytes:
|
||||
status: implemented
|
||||
value: 10485760
|
||||
evidence:
|
||||
- api/server.ts:30
|
||||
upload_max_bytes:
|
||||
status: implemented
|
||||
value: 10485760
|
||||
evidence:
|
||||
- api/middlewares/index.ts:7
|
||||
database_characteristics:
|
||||
status: implemented
|
||||
notes: "SQLite 适合单机/轻量;并发与事务能力有限。"
|
||||
evidence:
|
||||
- openspec/project.md:70
|
||||
|
||||
compliance:
|
||||
data_minimization:
|
||||
status: partial
|
||||
stored_personal_data:
|
||||
- field: users.name
|
||||
- field: users.phone
|
||||
constraints:
|
||||
- "当前未见用户数据保留期限/删除流程的实现"
|
||||
gdpr_like_rights:
|
||||
status: not_implemented
|
||||
requirements:
|
||||
- "数据导出:提供用户个人数据导出能力(当前仅管理员数据导出,且范围为业务数据)"
|
||||
- "数据删除:支持按合规要求删除用户数据并处理关联记录"
|
||||
constraints:
|
||||
- "以上为合规目标要求;当前代码中未实现对应流程"
|
||||
|
||||
operability:
|
||||
configuration:
|
||||
status: implemented
|
||||
mechanism: "dotenv + system_configs 表"
|
||||
evidence:
|
||||
- openspec/project.md:25
|
||||
- api/models/systemConfig.ts:1
|
||||
|
||||
97
openspec/specs/tech_stack.yaml
Normal file
97
openspec/specs/tech_stack.yaml
Normal file
@@ -0,0 +1,97 @@
|
||||
version: 1
|
||||
id: tech_stack
|
||||
title: Technology Stack Baseline
|
||||
sources:
|
||||
project_md:
|
||||
path: openspec/project.md
|
||||
lines: "6-38"
|
||||
package_json:
|
||||
path: package.json
|
||||
|
||||
runtime:
|
||||
language: TypeScript
|
||||
module_system: ESM
|
||||
evidence:
|
||||
- package.json:5
|
||||
|
||||
frontend:
|
||||
framework:
|
||||
name: react
|
||||
version: "18.x"
|
||||
router:
|
||||
name: react-router-dom
|
||||
version: "6.x"
|
||||
build_tool:
|
||||
name: vite
|
||||
version: "4.x"
|
||||
ui:
|
||||
- name: antd
|
||||
version: "5.x"
|
||||
- name: tailwindcss
|
||||
version: "3.x"
|
||||
- name: tailwind-merge
|
||||
version: "3.x"
|
||||
state:
|
||||
- name: zustand
|
||||
version: "4.x"
|
||||
http_client:
|
||||
- name: axios
|
||||
version: "1.x"
|
||||
|
||||
backend:
|
||||
platform: nodejs
|
||||
framework:
|
||||
name: express
|
||||
version: "4.x"
|
||||
dev_runtime:
|
||||
- name: tsx
|
||||
purpose: "运行 TypeScript 后端入口"
|
||||
- name: nodemon
|
||||
purpose: "热重载"
|
||||
middlewares:
|
||||
- name: cors
|
||||
- name: multer
|
||||
purpose: "文件上传"
|
||||
data:
|
||||
database:
|
||||
engine: sqlite3
|
||||
mode: "file-based"
|
||||
utilities:
|
||||
- name: dotenv
|
||||
- name: uuid
|
||||
|
||||
file_processing:
|
||||
excel:
|
||||
- name: xlsx
|
||||
purpose: "Excel 导入/导出"
|
||||
|
||||
dev_workflow:
|
||||
scripts:
|
||||
dev: "concurrently \"npm run dev:api\" \"npm run dev:frontend\""
|
||||
dev_api: "nodemon --exec tsx api/server.ts"
|
||||
dev_frontend: "vite"
|
||||
check: "tsc --noEmit"
|
||||
build: "tsc && vite build"
|
||||
start: "node dist/api/server.js"
|
||||
ports:
|
||||
frontend_dev_default: 5173
|
||||
backend_dev_default: 3001
|
||||
proxy:
|
||||
frontend_to_backend:
|
||||
path_prefix: /api
|
||||
target: "http://localhost:3001"
|
||||
evidence:
|
||||
- openspec/project.md:29
|
||||
|
||||
build_output:
|
||||
directory: dist
|
||||
notes: "前后端构建产物共享 dist 目录(见 project.md 约定)。"
|
||||
|
||||
environment_variables:
|
||||
PORT:
|
||||
description: "后端监听端口"
|
||||
default: 3001
|
||||
DB_PATH:
|
||||
description: "SQLite 数据库文件路径"
|
||||
default: "data/survey.db"
|
||||
|
||||
Reference in New Issue
Block a user