This commit is contained in:
zlwu committed 2026-07-17 21:59:47 +08:00
1 parent 8be55c08e9
commit a903fcf6d4
75 files changed
+9547 -124

No files matched your search

@@ -7,6 +7,7 @@
- 新增 OpenSpec 变更 `zkxw-engineering-standards`,包含提案、设计说明、**八项能力规范**及落地任务清单。
- **不修改**业务代码;规范正文以需求 V0.3 第 3 节「技术要求」为硬约束,UI 细节以两份 UI 规范文档为权威展开(本变更中的 UI 条目为**跨端统一原则与引用约束**,像素级 Token 仍以原文档为准)。
- 工作流/审批与权限条目直接对齐需求文档 **§2.1.11 流程管理** 与 **§2.1.5 角色管理** 等章节。
- **增补(2026-07)**:`ui-conventions` / `api-conventions` / `architecture-conventions` 明确小程序附件基线为**仅拍照凭证**(`uni.chooseImage` + `uni.uploadFile` + 业务提交 `imageUrls`),与 `web-admin` 通用附件卡片化布局分工;参考叁强 `sqxp-wx` 凭证上传。
## Capabilities
@@ -382,6 +382,44 @@ FastAPI 路由 Query 参数若 Python 参数名为 `page_size`,SHALL 声明 `Q
- **WHEN** 在 `app/api/` 增加新的路由模块
- **THEN** 该模块 SHALL 在 `app/api/__init__.py` 与 `app/main.py` 中完成注册,并符合现有认证与租户上下文注入模式
### Requirement: 小程序拍照凭证上传接口
`miniprogram` 拍照凭证(仅图片,见 `ui-conventions`「小程序拍照凭证上传(基线)」)相关接口 SHALL 遵循下列约定;权威行为对齐叁强鲜配 `POST /api/upload/image` + 业务接口携带 `imageUrls`。
#### 上传接口
1. **方法与编码**:`POST` + `multipart/form-data`(文件上传例外,见上文「文件上传下载例外」)。
2. **表单字段**:文件字段名 SHALL 为 `file`;业务目录字段 SHALL 为 `folder`(`Form` 字符串,如 `delivery-evidence`、`avatars`)。
3. **鉴权**:SHALL 要求登录态(Bearer JWT 或与小程序一致的现有鉴权);未登录 SHALL 返回 401。
4. **成功响应**:统一信封下 `data` SHALL 至少包含 `url`(对象存储可访问地址);MAY 含 `filename`、`contentType`、`size`。前端以 `data.url` 为唯一回填依据。
5. **类型与大小**:SHALL 仅接受图片 MIME(至少覆盖 `image/jpeg`、`image/png`、`image/webp`;建议兼容 `image/heic`/`image/heif`);单文件大小上限建议 **10MB**(与叁强一致,业务另有约定时在接口文档写明)。
6. **路径落点**:山峡落地时 MAY 复用/对齐 `POST /api/upload/image`,或提供 `POST /api/mini/.../upload/image`;无论路径如何,小程序 `uni.uploadFile` 的 `url` 与后端注册路径 SHALL 一致,并完成路由注册(`app/api/__init__.py`、`app/main.py`)。
#### 加解密排除(强制)
`multipart` 上传路径 SHALL 列入服务端 `API_ENCRYPT_EXCLUDE_PATHS`(及前端若启用 API 加密时的排除清单)。微信小程序端若因运行时不保证 WebCrypto 而关闭客户端加密(叁强 `sqxp-wx/utils/request.js` 在 `MP-WEIXIN` 下 `API_ENCRYPT_ENABLED = false`),上传仍 SHALL 依赖 HTTPS + Bearer,SHALL NOT 对文件字节做 JSON AES 包装。
#### 业务关联
1. 业务确认类接口(如取货、送达、备货完成)请求体 SHALL 使用 JSON,图片以 **URL 数组** 传递(建议字段名 `imageUrls: string[]`)。
2. 服务端收到 URL 列表后 SHALL 校验归属与数量上限,再更新业务状态;需要附件台账时 SHALL 写入附件表(或等价持久化),`file_url` 存完整 URL;多 URL 落单字段时 SHALL 使用英文逗号分隔(见 `architecture-conventions`「附件与对象存储」)。
3. 业务接口 SHALL NOT 再接收同一确认动作中的 multipart 文件流。
#### Scenario: 小程序上传一张凭证图
- **WHEN** 已登录小程序用户对图片上传接口提交 `file` + `folder`
- **THEN** 响应 `code` SHALL 为成功码且 `data.url` 非空,对象存储中可按该 URL 访问图片
#### Scenario: 业务提交携带 imageUrls
- **WHEN** 用户确认取货/送达等动作且本地已有已上传 URL
- **THEN** 业务 POST 体 SHALL 含 `imageUrls` 数组,服务端 SHALL 据此落库/关联,SHALL NOT 要求客户端再次上传二进制
#### Scenario: 上传路径未排除加密
- **WHEN** 上传路径未加入 `API_ENCRYPT_EXCLUDE_PATHS` 且全局 API 加密开启
- **THEN** 视为规范违规;实现与评审 SHALL 在合入前补齐排除项
### Requirement: 细胞存储样本跨表关联键
细胞存储场景下,将协议样本行(`biz_protocol_sample`)与下游业务(样本接收 `biz_sample_reception`、质检工单 `biz_qc_work_order` 等)进行**业务匹配**时,关联键 SHALL 仅为 `tenant_no` + `sample_code`(租户内样本编码唯一且不变;接收/工单表须持久化 `sample_code` 快照)。**不得**使用 `collection_protocol_id` 或 `protocol_sample_id` 作为查询、列表排除、详情回显的关联依据(协议单可换号/转科研、编辑保存会重建样本行 `id`,但样本编码不变)。
@@ -22,11 +22,21 @@
业务附件 SHALL 通过对象存储(如 MinIO)保存可访问 URL;多附件在同一字段内存储时 SHALL 使用英文逗号分隔多个 URL(与需求 §3 一致)。
端侧分工:
- **`web-admin`**:通用附件(图片/视频/办公文档等)按 `ui-conventions`「附件上传卡片化布局」上传与展示。
- **`miniprogram`(当前基线)**:仅拍照/相册**图片凭证**,按 `ui-conventions`「小程序拍照凭证上传(基线)」与 `api-conventions`「小程序拍照凭证上传接口」;文档类附件不在小程序基线范围内。
#### Scenario: 上传列表回显
- **WHEN** 业务实体包含多个附件
- **THEN** 持久化字段 SHALL 为逗号分隔的 URL 字符串,且接口契约与前端解析逻辑一致
#### Scenario: 小程序与 Web 附件能力边界
- **WHEN** 同一业务在 Web 与小程序均需留存材料
- **THEN** 小程序侧 SHALL 仅实现拍照凭证 URL 链路;若业务必须上传 PDF 等文档,SHALL 引导在 Web 完成或先扩展 OpenSpec 后再实现
### Requirement: 权威文档索引
实现类变更 SHALL 将 `工程规范`、`docs/web-admin-ui-spec-fresh-mint.md`、`docs/miniprogram-ui-spec-sanqiang.md` 及 `AGENTS.md` 视为架构与规范的索引入口;多系统 SQL 与 CRM/地磅边界 SHALL 以 `ecosystem-landscape` spec 为准;OpenSpec 变更 SHALL 在 `proposal.md` / `design.md` 中保持对上述路径的引用。
@@ -923,6 +923,129 @@ watchDebounced(
- **WHEN** PR 新增或修改附件上传 UI
- **THEN** 审查者 SHALL 对照 `ProtocolFormDialog.vue` 确认卡片化布局与立即上传逻辑
- **THEN** SHALL 拒绝 `picture-card`、`show-file-list` 默认列表、与规范不一致的尺寸/色值
---
### Requirement: 小程序拍照凭证上传(基线)
`miniprogram` 业务附件在当前产品范围内 **仅支持拍照/相册图片凭证**(如取货、送达、备货、现场确认等),**SHALL NOT** 实现 PDF/Word/Excel 等文档类附件上传(文档类仍以 `web-admin`「附件上传卡片化布局」为准)。实现与交互以叁强鲜配 `sqxp-wx` 配送/供应商凭证上传为权威参考(如 `pages/deliverer/task/detail/detail.vue`、`pages/supplier/delivery/detail/detail.vue`、`pages/supplier/settings/avatar.vue`);山峡侧后续落地时 SHALL 抽公共工具(建议 `miniprogram/utils/upload.js`),业务页只负责数量上限、`folder` 与提交字段。
#### 范围与禁止项
1. **允许来源**:`uni.chooseImage`(或等价的 `uni.chooseMedia` 且仅 `mediaType: ['image']`),`sourceType` 含 `album` 与 `camera`,`sizeType` 优先 `compressed`。
2. **禁止**:SHALL NOT 使用 `wx.chooseMessageFile` / `uni.chooseFile` 选聊天文件或任意文档;SHALL NOT 复用 Web 端 `el-upload`、`FormData` + `uni.request`/`fetch` 上传二进制;SHALL NOT 把图片转 Base64 塞进 JSON 业务接口(除非产品书面要求且接口单独评审)。
3. **色板与触控**:上传入口、删除热区 SHALL 遵守小程序 Token(品牌绿 `#0a9f6d` 系)与最小触控 44×44;SHALL NOT 使用 Web Fresh Mint `#10b981` 作为小程序主色。
#### 两步流程(强制)
1. **选图并立即上传**:用户选择临时路径后,逐张(或受控并发)调用 `uni.uploadFile` 上传到图片上传接口,成功后将返回的 **URL 字符串** 推入本地数组(如 `imageUrls`)。
2. **业务提交**:用户确认业务动作(取货/送达/保存等)时,再调用业务 API,请求体携带已上传的 URL 列表(字段名建议 `imageUrls`,camelCase);服务端再关联业务并(如需要)写入附件表。SHALL NOT 在业务提交接口中直接接收 multipart 文件流(与叁强「先 URL、后业务」一致)。
#### `uni.uploadFile` 约定
| 项 | 要求 |
|----|------|
| 调用方式 | `uni.uploadFile`(微信小程序原生 multipart) |
| `url` | 图片上传接口绝对或与 `API_BASE` 拼接后的完整 URL(见 `api-conventions`「小程序拍照凭证上传接口」) |
| `filePath` | `chooseImage` / `chooseMedia` 返回的临时路径 |
| `name` | 固定为 `file`(与后端 `UploadFile` 字段名一致) |
| `header.Authorization` | `Bearer ${token}`;token 来自本地存储(与登录态同一 key) |
| `formData.folder` | 业务目录字符串(如 `delivery-evidence`、`avatars`、山峡业务自定义 kebab/短路径) |
| 响应解析 | `uploadFile` 的 `data` 多为 **字符串**,SHALL `JSON.parse` 后再读 `code` / `data.url`;成功条件与统一响应一致(`code === 200` 且存在 `data.url`) |
| Loading | 上传过程 SHALL `uni.showLoading`,结束 `uni.hideLoading`;失败 SHALL `uni.showToast` 提示 |
上传逻辑 **SHALL NOT** 走会做 AES/包体加密的通用 `request()` 封装(multipart 无法套 JSON 加密封装);鉴权仅靠 Header Bearer。
#### UI 与数量
1. **展示**:已上传项以缩略图网格/横向列表展示;点击预览 SHALL 使用 `uni.previewImage`(`urls` 为当前列表,`current` 为点击项)。
2. **删除**:未提交业务前可从本地数组移除;已随业务提交锁定的凭证按业务只读展示,SHALL NOT 在只读态提供删除。
3. **数量上限**:单次业务凭证默认最多 **6** 张(与叁强配送凭证一致);若业务需要其他上限,SHALL 在页面文案与选图 `count` 中写死同一常量。选图前 `count = 上限 - 已有张数`,已满则 toast「最多上传 N 张」。
4. **上传入口文案**:建议「+ 上传」或「选择图片并上传」;入口热区 ≥ 44×44。
#### 参考实现骨架(工具函数形态)
```javascript
// miniprogram/utils/upload.js(后续落地时抽取;业务页传入 folder / maxCount)
export async function chooseAndUploadImages({ folder, maxCount, currentUrls, token, uploadUrl }) {
const remain = maxCount - currentUrls.length
if (remain <= 0) {
uni.showToast({ title: `最多上传${maxCount}张`, icon: 'none' })
return []
}
const choose = await new Promise((resolve) => {
uni.chooseImage({
count: remain,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => resolve(res),
fail: () => resolve(null)
})
})
if (!choose?.tempFilePaths?.length) return []
const urls = []
uni.showLoading({ title: '上传中...' })
try {
for (const filePath of choose.tempFilePaths) {
const uploadRes = await new Promise((ok, fail) => {
uni.uploadFile({
url: uploadUrl,
filePath,
name: 'file',
header: { Authorization: token ? `Bearer ${token}` : '' },
formData: { folder },
success: ok,
fail
})
})
const raw = typeof uploadRes.data === 'string' ? uploadRes.data : String(uploadRes.data || '')
let body = {}
try {
body = JSON.parse(raw)
} catch {
body = {}
}
if (body.code !== 200 || !body.data?.url) {
uni.showToast({ title: body.message || '上传失败', icon: 'none' })
break
}
urls.push(String(body.data.url))
}
} finally {
uni.hideLoading()
}
return urls
}
```
业务页在确认动作时示例:
```javascript
await request({
url: `/mini/<role>/tasks/${taskId}/pickup`,
method: 'POST',
data: { remark: '已取货', imageUrls: pickupImages.value }
})
```
#### Scenario: 配送/现场凭证上传
- **WHEN** 用户在小程序业务页点击「上传」且未达上限
- **THEN** SHALL 调起相册或拍照选图,并对每张临时文件调用 `uni.uploadFile`
- **THEN** 成功后本地列表增加可预览的图片 URL,业务提交时携带 `imageUrls`
#### Scenario: 禁止文档类附件
- **WHEN** 需求表述为「附件」但场景为小程序端
- **THEN** 实现 SHALL 仅提供拍照凭证能力;若确需 PDF 等文档,SHALL 先走独立 OpenSpec 变更扩展本 Requirement,不得静默加入 `chooseMessageFile`
#### Scenario: Code review 小程序凭证上传
- **WHEN** PR 新增或修改 `miniprogram` 图片/凭证上传
- **THEN** 审查者 SHALL 确认使用 `uni.chooseImage`(或仅图片的 `chooseMedia`)+ `uni.uploadFile`,且业务提交为 URL 列表
- **THEN** SHALL 拒绝 `FormData`+`request`、Base64 塞业务 JSON、以及未排除加解密的上传路径误用
### Requirement: 按钮须携带图标
`web-admin` 中所有带文字标签的 `el-button`(或 `ElButton`)SHALL 在文字**前方**包含 `<el-icon>` 子元素,使用 `@element-plus/icons-vue` 提供的图标组件;SHALL NOT 出现仅有文字而无图标的按钮。该规则适用于列表工具栏、表格操作列、`el-dialog` / 抽屉的 `#footer` 底栏、表单内 `link` / `plain` / `size="small"` 等**凡渲染可见文案的按钮**,不限于 `type="primary"`。图标的引入方式为在文件 `<script setup>` 中显式 `import` 所需图标组件(`@element-plus/icons-vue` 不属于 auto-import 范围)。图标与文字间距由全局样式保证(与 Fresh Mint 弹窗底栏约定一致,约 6px)。
@@ -9,6 +9,7 @@
- [x] 2.1 在 `openspec/config.yaml` 的 `context` 中摘要本变更及 `ecosystem-landscape`(多系统 SQL 与 CRM 路径);OpenCode 见 `.opencode/ecosystem-landscape.md`
- [ ] 2.2 在 PR 模板或评审清单中增加一条:涉及租户/软删/排序的改动必须对照 `api-conventions` 与 `database-conventions`
- [ ] 2.3 工作流专项启动前,将 `workflow-conventions` 中 SQL 审批人、版本迁移等「Open Questions」拆成独立变更
- [x] 2.4 将小程序「仅拍照凭证」上传写法写入 `ui-conventions` / `api-conventions` / `architecture-conventions`,并在 `AGENTS.md` 摘要索引(后续 `miniprogram` 业务开发以此为基线)
## 3. 归档(实现稳定后)