Skip to content

Commit e1e8ecc

Browse files
committed
feat: 更新工作流 API 合同,补充日期时间字段的 RFC3339 格式描述,优化错误信息返回
1 parent fbcd810 commit e1e8ecc

11 files changed

Lines changed: 54 additions & 29 deletions

File tree

.specify/memory/constitution.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
<!--
22
Sync Impact Report
3-
- Version change: 1.0.0 -> 1.1.0
3+
- Version change: 1.1.0 -> 1.2.0
44
- Modified principles:
5-
- V. Safe Query and Debuggability -> V. Safe Query, API Errors, and Debuggability
6-
- Delivery Workflow & Quality Gates: added breaking-change migration rule
7-
- Added sections:
85
- None
6+
- Added sections:
7+
- VI. DRF Serializer-First API Responses
98
- Removed sections:
109
- None
1110
- Follow-up TODOs:
@@ -53,6 +52,16 @@ flows MUST assert both success and failure paths. Rationale: predictable debuggi
5352
safe execution, and controlled error disclosure are core trust requirements for a
5453
database operations platform.
5554

55+
### VI. DRF Serializer-First API Responses
56+
API implementations SHOULD return response data through Django REST Framework
57+
serializers when fields require typed representation, validation, or public
58+
contract stability. Endpoints MUST NOT hand-encode JSON or bypass serializers for
59+
typed fields such as datetimes, identifiers, booleans, decimals, or nested
60+
contract objects unless the exception is explicitly documented and covered by a
61+
targeted test. Rationale: serializer-driven responses keep runtime output,
62+
OpenAPI schemas, and generated clients aligned, and prevent ad hoc JSON encoding
63+
from producing field formats that violate the public API contract.
64+
5665
## Testing Standards
5766

5867
- Test suites MUST default to pytest invocation and naming conventions configured in
@@ -90,4 +99,4 @@ Versioning policy follows semantic versioning for governance:
9099
Compliance review is mandatory in planning and PR review. Constitution checks in
91100
planning artifacts MUST pass or carry an explicit, approved exception record.
92101

93-
**Version**: 1.1.0 | **Ratified**: 2026-04-28 | **Last Amended**: 2026-08-31
102+
**Version**: 1.2.0 | **Ratified**: 2026-04-28 | **Last Amended**: 2026-09-03

specs/005-workflow-api-contract/contracts/workflow-api-contract.openapi.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,8 @@ components:
265265
create_time:
266266
type: string
267267
format: date-time
268+
description: RFC3339-compatible timestamp rendered with a UTC suffix.
269+
example: '2017-07-21T17:32:28Z'
268270
nullable: true
269271
WorkflowContent:
270272
type: object
@@ -307,6 +309,8 @@ components:
307309
operation_time:
308310
type: string
309311
format: date-time
312+
description: RFC3339-compatible timestamp rendered with a UTC suffix.
313+
example: '2017-07-21T17:32:28Z'
310314
WorkflowLogList:
311315
type: object
312316
additionalProperties: true

specs/005-workflow-api-contract/spec.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
- Q: 现有 SQL Workflow logs/status/approval/execution API 是否缺少 OpenAPI request/response body 契约? → A: 是;补齐现有接口 schema,不新增重复接口。
1313

14+
### Session 2026-09-03
15+
16+
- Q: API responses that document fields as `date-time` should use which wire format? → A: Preserve the existing datetime value and render it as an RFC3339-compatible UTC-suffixed string, such as `2017-07-21T17:32:28Z`.
17+
1418
## User Scenarios & Testing *(mandatory)*
1519

1620
### User Story 1 - Preserve CLI Review Text From Workflow Results (Priority: P1)
@@ -98,6 +102,7 @@ As a CLI or generated-client consumer, I can rely on a documented extension stra
98102
- **FR-019**: The existing SQL Workflow approval endpoint MUST document that the acting user and workflow are derived from the authenticated session and path audit identifier, that the request body may contain `audit_remark`, and that the response body is the standard action result.
99103
- **FR-020**: The existing SQL Workflow execution endpoint MUST document that the acting user and workflow are derived from the authenticated session and path audit identifier, that the request body requires execution `mode`, and that the response body is the standard action result.
100104
- **FR-021**: SQL Workflow approval and execution contracts MUST NOT require or document an `engineer` request field for these existing audit-id endpoints.
105+
- **FR-022**: API response fields documented with OpenAPI `format: date-time`, including workflow `create_time` and log `operation_time`, MUST preserve the existing datetime value and render it as an RFC3339-compatible UTC-suffixed string ending in `Z`.
101106

102107
### Test Strategy Constraints *(mandatory)*
103108

@@ -126,6 +131,7 @@ As a CLI or generated-client consumer, I can rely on a documented extension stra
126131
- **SC-005**: Existing CLI review text and structured review JSON fixtures remain unchanged for all covered happy-path workflow examples.
127132
- **SC-006**: 100% of unauthorized, missing, and invalid instance or workflow lookup tests return sanitized deterministic failures.
128133
- **SC-007**: Generated OpenAPI output shows response bodies for SQL Workflow logs and status, and request/response bodies for SQL Workflow approval and execution, in 100% of contract validation runs.
134+
- **SC-008**: 100% of workflow API `date-time` response fields checked in this feature end in `Z` and parse as RFC3339-compatible timestamps.
129135

130136
## Assumptions
131137

sql/archiver.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def archive_apply(request):
215215
try:
216216
audit_handler.create_audit()
217217
except AuditException as e:
218-
logger.error(f"新建审批流失败: {str(e)}")
218+
logger.info("新建审批流失败, reason=%s", e)
219219
return JsonResponse(
220220
{"status": 1, "msg": "新建审批流失败, 请联系管理员", "data": {}}
221221
)
@@ -278,7 +278,7 @@ def archive_audit(request):
278278
audit_status, request.user, audit_remark
279279
)
280280
except AuditException as e:
281-
return render(request, "error.html", {"errMsg": f"审核失败: {str(e)}"})
281+
return render(request, "error.html", {"errMsg": f"审核失败: {e}"})
282282
auditor.workflow.status = auditor.audit.current_status
283283
if auditor.audit.current_status == WorkflowStatus.PASSED:
284284
auditor.workflow.state = True

sql/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,11 @@ def get_audit(self) -> Optional["WorkflowAudit"]:
316316
except WorkflowAudit.DoesNotExist:
317317
return None
318318

319+
@property
320+
def audit_id(self) -> Optional[int]:
321+
audit = self.get_audit()
322+
return audit.audit_id if audit else None
323+
319324

320325
class SqlWorkflow(models.Model, WorkflowAuditMixin):
321326
"""

sql/query_privileges.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ def query_priv_apply(request):
312312
with transaction.atomic():
313313
audit_handler.create_audit()
314314
except AuditException as e:
315-
logger.error(f"新建审批流失败, {str(e)}")
315+
logger.info("新建审批流失败, reason=%s", e)
316316
result["status"] = 1
317317
result["msg"] = "新建审批流失败, 请联系管理员"
318318
return HttpResponse(json.dumps(result), content_type="application/json")
@@ -482,7 +482,7 @@ def query_priv_audit(request):
482482
audit_status, request.user, audit_remark
483483
)
484484
except AuditException as e:
485-
return render(request, "error.html", {"errMsg": f"审核失败: {str(e)}"})
485+
return render(request, "error.html", {"errMsg": f"审核失败: {e}"})
486486
# 统一 call back, 内部做授权和更新数据库内容
487487
_query_apply_audit_call_back(
488488
auditor.audit.workflow_id, auditor.audit.current_status

sql/utils/workflow_audit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434

3535
class AuditException(Exception):
36-
pass
36+
"""审核流程中的业务异常,异常消息会直接展示给终端用户。"""
3737

3838

3939
class ReviewNodeType(Enum):

sql_api/api_workflow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ def post(self, request):
257257
action, user, serializer.data["audit_remark"]
258258
)
259259
except AuditException as e:
260-
raise serializers.ValidationError({"errors": f"操作失败, {str(e)}"})
260+
raise serializers.ValidationError({"errors": f"操作失败, {e}"})
261261

262262
# 最后处置一下原本工单的状态
263263
if auditor.workflow_type == WorkflowType.QUERY:

sql_api/api_workflow_operations.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -416,9 +416,9 @@ def post(self, request, audit_id):
416416
detail = auditor.operate(
417417
WorkflowAction.PASS, request.user, data["audit_remark"]
418418
)
419-
except AuditException:
420-
logger.exception("审核工单失败,audit_id=%s", audit_id)
421-
raise ValidationError({"detail": "审核工单失败"})
419+
except AuditException as e:
420+
logger.info("审核工单失败,audit_id=%s, reason=%s", audit_id, e)
421+
raise ValidationError({"detail": f"审核工单失败, 失败原因: {e}"})
422422
if auditor.audit.current_status == WorkflowStatus.PASSED:
423423
auditor.workflow.status = "workflow_review_pass"
424424
auditor.workflow.save(update_fields=["status"])
@@ -449,9 +449,9 @@ def post(self, request, audit_id):
449449
detail = auditor.operate(
450450
WorkflowAction.REJECT, request.user, data["reject_remark"]
451451
)
452-
except AuditException:
453-
logger.exception("拒绝工单失败,audit_id=%s", audit_id)
454-
raise ValidationError({"detail": "拒绝工单失败"})
452+
except AuditException as e:
453+
logger.info("拒绝工单失败,audit_id=%s, reason=%s", audit_id, e)
454+
raise ValidationError({"detail": f"拒绝工单失败, 失败原因: {e}"})
455455
workflow.status = "workflow_abort"
456456
workflow.save(update_fields=["status"])
457457
if was_scheduled:
@@ -587,9 +587,9 @@ def post(self, request, audit_id):
587587
auditor = get_auditor(workflow=workflow, sys_config=config)
588588
try:
589589
detail = auditor.operate(action, request.user, data["cancel_remark"])
590-
except AuditException:
591-
logger.exception("取消工单失败,audit_id=%s", audit_id)
592-
raise ValidationError({"detail": "终止工单失败"})
590+
except AuditException as e:
591+
logger.info("取消工单失败,audit_id=%s, reason=%s", audit_id, e)
592+
raise ValidationError({"detail": f"终止工单失败, 失败原因: {e}"})
593593
workflow.status = "workflow_abort"
594594
workflow.save(update_fields=["status"])
595595
if was_scheduled:
@@ -661,4 +661,7 @@ def get(self, request, audit_id):
661661
"operation_time",
662662
)
663663
)
664-
return Response({"total": len(rows), "rows": rows})
664+
serializer = WorkflowLogListResponseSerializer(
665+
{"total": len(rows), "rows": rows}
666+
)
667+
return Response(serializer.data)

sql_api/serializers.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -470,17 +470,13 @@ class Meta:
470470

471471

472472
class SqlWorkflowDetailSerializer(serializers.ModelSerializer):
473-
audit_id = serializers.SerializerMethodField()
473+
audit_id = serializers.IntegerField(read_only=True, allow_null=True)
474474
workflow_id = serializers.IntegerField(source="id", read_only=True)
475475
instance_name = serializers.CharField(
476476
source="instance.instance_name", read_only=True
477477
)
478478
status_display = serializers.CharField(source="get_status_display", read_only=True)
479479

480-
def get_audit_id(self, obj):
481-
audit = obj.get_audit()
482-
return audit.audit_id if audit else None
483-
484480
class Meta:
485481
model = SqlWorkflow
486482
fields = [

0 commit comments

Comments
 (0)