作为 xiaoqu-agent-service(MCP 服务) 的开发参考。新系统与旧系统零代码耦合, 仅照搬其行级数据权限规则与数据源结构。设计方案见 xiaoqu-franchisee/doc/agent-mcp-service-design.md
20 KiB
开发文档:出库单上传凭证
需求:在「全部出库单」(web_id=211) 每行新增「上传凭证」按钮。
- 弹窗可上传图片 / PDF + 填备注(最多 10 个,单文件 ≤ 100MB,仅前端校验);
- 打开时若已上传过则回显文件列表 + 备注;
- 弹窗内点 ❌ 不真删,点「提交」时才落库;
- 「提交」自动判定新增 / 更新。 环境:dev
192.168.1.181:3306(root/kaixinjiuhao),PRD 上线需把authority_id替换为 PRD 实际值后重跑。 涉及库:xiaoqu_franchisee_d(业务数据)+xiaoqu_comples_d(按钮权限)。
1. 数据库变更
1.1 业务表 — xiaoqu_franchisee_d
1.1.1 新表 warehouse_out_certificate(出库单凭证 - 备注)
图片本身复用现有 warehouse_in_out_pic,本表只保存 备注。约定:
warehouse_in_out_pic.aggregate_id = warehouse_out.idwarehouse_in_out_pic.file_type = 4(新枚举值:出库凭证)warehouse_in_out_pic.in_out_allocation_type = 99(新枚举值:出库凭证类型,避开 0/1/2/10-14 已用值)
USE xiaoqu_franchisee_d;
CREATE TABLE `warehouse_out_certificate` (
`id` bigint(8) NOT NULL AUTO_INCREMENT COMMENT '主键',
`out_order_id` bigint(8) NOT NULL COMMENT '出库单ID(warehouse_out.id)',
`out_order_no` varchar(50) DEFAULT '' COMMENT '出库单号(冗余,便于查询)',
`memo` varchar(500) DEFAULT '' COMMENT '备注信息',
`create_id` bigint(8) DEFAULT '0' COMMENT '创建id',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`modify_id` bigint(8) DEFAULT '0' COMMENT '修改id',
`modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`soft_delete` int(4) DEFAULT '0' COMMENT '数据状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_out_order_id` (`out_order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出库单凭证(备注)';
uk_out_order_id唯一索引保证一张出库单只有一条凭证记录;图片可多张挂warehouse_in_out_pic。
1.1.2 扩充 warehouse_in_out_pic 字段枚举注释(可选 / 可不执行)
仅刷注释、便于阅读:
ALTER TABLE `warehouse_in_out_pic`
MODIFY COLUMN `file_type` int(4) DEFAULT '0'
COMMENT '文件类型0:申请图片;1:入库确认图片;2:出库确认图片;3:送货工单签收;4:出库凭证',
MODIFY COLUMN `in_out_allocation_type` int(4) DEFAULT '0'
COMMENT '出入库类型(0:入库;1:出库;2:调拨;10:装机工单;11:送货工单;12:取货工单;13:营销客户申请;14:营销单申请;99:出库凭证)';
1.2 按钮权限(权限库)
「上传凭证」是行内按钮(type=0),挂在「全部出库单」(web_id=211) 上,按钮权限复用该页面当前已有的授权范围(不写死 authority_id,避免环境差异)。
环境对应的权限 schema:
- dev →
xiaoqu_comples_d- test →
test- prod →
xiaoqu_comples_p切环境只需改第一行
USE,其余语句通用。
各环境实测授权范围(2026-05-14 核对):
- dev (
xiaoqu_comples_d)web_id=211→ 授权 2 个 authority:1, 102- test (
test)web_id=211→ 授权 26 个 authority(导自线上)- prod (
xiaoqu_comples_p) 上线前再用下面 §4 校验脚本现查一次
USE xiaoqu_comples_d; -- dev;test 改 USE test; prod 改 USE xiaoqu_comples_p;
START TRANSACTION;
-- ─────────────────────────────────────────────────────────────
-- ① button:按钮本体
-- type=0 行内按钮 / type=1 顶部 tableButton
-- theme: primary(蓝) / danger(红) / info(灰) / 空
-- 注意:不要硬编码 id,让 AUTO_INCREMENT 决定(不同 schema id 必然不同)
-- ─────────────────────────────────────────────────────────────
INSERT INTO `button`
(web_id, cn_name, en_name, icon, class_name, function_path,
`type`, `status`, order_by, theme, memo, app_en_name)
VALUES
(211, '上传凭证', 'uploadCertificate', '', '', '',
0, 1, 0, 'primary', '出库单上传凭证(图片+备注)', '');
SET @new_button_id := LAST_INSERT_ID();
SELECT @new_button_id AS new_button_id;
-- ─────────────────────────────────────────────────────────────
-- ② button_webpage_authority:动态复用 web_id=211 当前所有授权组
-- 这样不管 dev/test/prod,新按钮始终对所有能进该页的用户可见
-- ─────────────────────────────────────────────────────────────
INSERT INTO `button_webpage_authority`
(web_id, authority_id, cn_name, en_name, button_id,
order_by, check_status, param, icon, `type`, class_name, theme, memo)
SELECT
211,
wa.authority_id,
'上传凭证',
'uploadCertificate',
@new_button_id,
0, 1, '', '', 0, '', 'primary', ''
FROM (
SELECT DISTINCT authority_id
FROM `webpage_authority`
WHERE web_id = 211
AND soft_delete = 0
) wa;
-- ─────────────────────────────────────────────────────────────
-- ③ 校验
-- ─────────────────────────────────────────────────────────────
SELECT id, web_id, cn_name, en_name, `type`, theme FROM `button` WHERE id = @new_button_id;
SELECT authority_id, COUNT(*) AS cnt FROM `button_webpage_authority`
WHERE button_id = @new_button_id AND soft_delete=0
GROUP BY authority_id;
SELECT COUNT(*) AS total_grants FROM `button_webpage_authority`
WHERE button_id = @new_button_id AND soft_delete=0;
-- total_grants 应等于「全部出库单」当前的 webpage_authority 数:
-- dev=2 / test=26 / prod=待现查
COMMIT;
-- 失败请 ROLLBACK
2026-05-14 已执行情况:
- dev (
xiaoqu_comples_d):button.id=789,2 条 button_webpage_authority(authority 1, 102)- test (
test):button.id=790,26 条 button_webpage_authority(authority 1, 22, 23, 32, 34, 41, 49, 50, 52, 55, 56, 64, 65, 66, 68, 70, 71, 75, 85, 92, 94, 96, 105, 108, 118, 120)
2. 后端改动 — xiaoqu-franchisee
2.1 包结构
xiaoqu-publicbeanandmap/src/main/java/me/iiv/iivframework/
├── beans/b2/WarehouseOutCertificate.java # @TableName("warehouse_out_certificate")
├── mapper/m2/WarehouseOutCertificateMapper.java # extends BaseMapper
└── service/s2/
├── WarehouseOutCertificateService.java # interface IService
└── Impl/WarehouseOutCertificateServiceImpl.java # extends ServiceImpl
xiaoqu-franchisee/src/main/java/me/iiv/iivframework/controller/warehouse/
├── WarehouseOutController.java # 追加 3 个方法
├── input/OutCertificateSaveInput.java # 新建
└── output/OutCertificateOutput.java # 新建(位置参考已有 WarehouseInOutPicOut 同包)
文件
OutCertificateOutput.java/OutCertificateSaveInput.java实际路径要与项目既有约定一致,参考me.iiv.iivframework.controller.output.WarehouseInOutPicOut/me.iiv.iivframework.controller.insert.WarehouseInOutPicInsert。
2.2 API(新增 2 个端点)
挂在 WarehouseOutController 下(@RequestMapping("/myWarehouseOut")):
| Method | URL | 说明 |
|---|---|---|
| GET | /myWarehouseOut/certificate/get |
查询出库单的凭证(备注+图片列表) |
| POST | /myWarehouseOut/certificate/save |
保存(自动 upsert 备注 + 真删用户去掉的图片 + 绑定新图片) |
2.2.1 GET /certificate/get
Query
outOrderId: Long
Output
public class OutCertificateOutput {
private Long outOrderId;
private String memo; // 无记录则空串
private List<WarehouseInOutPicOut> picList; // 复用现有 DTO
}
逻辑
WarehouseOutCertificate cert = certService.getOne(
Wrappers.<WarehouseOutCertificate>lambdaQuery()
.eq(WarehouseOutCertificate::getOutOrderId, outOrderId)
.eq(WarehouseOutCertificate::getSoftDelete, 0)
.last("limit 1"));
List<WarehouseInOutPic> pics = warehouseInOutPicService.list(
Wrappers.<WarehouseInOutPic>lambdaQuery()
.eq(WarehouseInOutPic::getAggregateId, outOrderId)
.eq(WarehouseInOutPic::getFileType, 4)
.eq(WarehouseInOutPic::getInOutAllocationType, 99)
.eq(WarehouseInOutPic::getSoftDelete, 0));
// for each pic: OSSClientUtil.getNormalImageUrl(bucket, folder, fileName) → 组装 WarehouseInOutPicOut
2.2.2 POST /certificate/save
Body (JSON)
public class OutCertificateSaveInput {
private Long outOrderId; // 必填
private String memo; // ≤500
private List<Long> fileIds; // 当前保留的图片 fileId 列表(前端 ❌ 掉的不传)
}
逻辑(一次性 upsert,幂等)
0. 校验:outOrderId 必填、memo ≤500、fileIds 元素必须均归属本出库单或 aggregate_id=0 的孤儿(防越权改别人的图)。
1. 备注 upsert
- SELECT … WHERE out_order_id=? AND soft_delete=0
- 存在 → UPDATE memo / modify_id / modify_time
- 不存在 → INSERT (out_order_id, out_order_no(冗余取自 warehouse_out), memo, …)
2. 真删被去掉的图片
- dbIds = warehouse_in_out_pic WHERE aggregate_id=outOrderId AND file_type=4 AND in_out_allocation_type=99 AND soft_delete=0
- toDelete = dbIds - fileIds
- 对 toDelete:
a) SET soft_delete=1, modify_time=NOW()
b) OSSClientUtil.deleteFile(bucket, folder, fileName) // 参考 UploadFileController.deleteFile
3. 绑定新上传的图片
- newOnes = fileIds 中 aggregate_id=0 的(即刚通过 /uploadFile/warehouseCertificate 落库的孤儿)
- UPDATE warehouse_in_out_pic
SET aggregate_id=outOrderId, file_type=4, in_out_allocation_type=99,
modify_id=?, modify_time=NOW()
WHERE id IN (newOnes) AND aggregate_id=0 AND soft_delete=0
⚠️ 上传接口
/uploadFile/warehouseCertificate是通用接口(同样用于装机/送货/调拨等),落库时不设aggregate_id/file_type/in_out_allocation_type,因此必须靠"提交"时绑定。 ⚠️ 用户点「返回」不提交时,这次上传的图片在 DB 里aggregate_id=0,会成为孤儿 → 见 §2.4。
2.3 Service 接口要点
public interface WarehouseOutCertificateService extends IService<WarehouseOutCertificate> {
OutCertificateOutput getByOutOrderId(Long outOrderId);
void save(Long userId, OutCertificateSaveInput input);
}
事务:save() 加 @Transactional(备注 upsert + 图片软删/绑定 必须原子)。
2.4 孤儿图片清理(可选 / 推荐)
用户连续上传多张图后点「返回」会留下 aggregate_id=0 的记录。加一个 XXL-Job 定时任务(参考 task/AutoOutWarehouseTask.java 同包结构):
每天凌晨:
SELECT id, ossbucketname, ossfolder, file_name
FROM warehouse_in_out_pic
WHERE aggregate_id=0
AND soft_delete=0
AND create_time < NOW() - INTERVAL 1 DAY;
→ 软删 + OSSClientUtil.deleteFile
如本期不做,先记录到运维 backlog。
3. 前端改动 — vue-manage-system
3.1 API 端点
src/api/api.js → outManage 块追加:
certificateGet: '/myWarehouseOut/certificate/get',
certificateSave: '/myWarehouseOut/certificate/save',
src/api/warehouseService.js → outManageService:
certificateGet: (params) => request.get($api.outManage.certificateGet, { params }),
certificateSave: (data) => request.post($api.outManage.certificateSave, data),
3.2 新建组件:outCertificateForm.vue
路径:src/components/page/warehouseInOutManage/outManage/components/outCertificateForm.vue
props:outOrderId: Number;emit:success、cancel
骨架:
<template>
<div class="cert-form">
<!-- 上传区:支持图片 + PDF;picList 含图片/PDF 两类,统一用文件名样式 -->
<el-upload
class="cert-upload"
:action="uploadUrl"
:headers="uploadHeaders"
name="image"
:show-file-list="false"
:limit="10"
:before-upload="beforeUpload"
:on-success="handleUploadSuccess"
accept="image/jpeg,image/jpg,image/png,image/gif,image/bmp,application/pdf"
>
<div class="upload-trigger"><i class="el-icon-plus"></i><span>上传</span></div>
</el-upload>
<div class="tip"><b>上传凭证</b> 最多10个文件,每个文件大小限制100M</div>
<el-form label-width="60px">
<el-form-item label="备注:">
<el-input
v-model="memo"
type="textarea"
:rows="6"
maxlength="500"
show-word-limit
placeholder="请输入备注信息(选填)" />
</el-form-item>
</el-form>
<el-divider content-position="center">凭证</el-divider>
<div v-if="picList.length === 0" class="empty">暂无数据</div>
<div v-else class="pic-list">
<span v-for="(p, i) in picList" :key="p.fileId" class="pic-item" @click="preview(p)">
<i :class="isPdf(p.fileName) ? 'el-icon-document' : 'el-icon-picture-outline'"></i>
{{ p.fileName }}
<i class="el-icon-error remove" @click.stop="removePic(i)"></i> <!-- ❌ 仅本地移除 -->
</span>
</div>
<div class="footer">
<el-button @click="$emit('cancel')">返回</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">提交</el-button>
</div>
</div>
</template>
关键交互:
mounted→ 调outManageService.certificateGet({outOrderId})→ 回显memo+picListbeforeUpload(file)前端校验(仅前端,不走后端兜底):- 扩展名必须在
['jpg','jpeg','png','gif','bmp','pdf'] - 大小 ≤
100 * 1024 * 1024(100MB) - 当前列表 + 待上传不超过 10 个
- 任一不满足
this.$message.error('...')+return false终止上传
- 扩展名必须在
- 上传成功回调把
{fileId, fileName, url}push 进picList,不调保存接口 removePic(i)仅splice,不调接口preview(p):图片走el-image-viewer大图预览;PDFwindow.open(p.url)新窗口打开- 「提交」:
certificateSave({outOrderId, memo, fileIds: picList.map(p=>p.fileId)})→ 成功 emitsuccess
3.3 outAllDoc.vue 接入
引入组件、加 dialog、在 buttonsClickFun 加 handler:
import outCertificateForm from './components/outCertificateForm.vue';
components: { ..., outCertificateForm },
data() { return {
...,
showCertificate: false,
certificateOrderId: null,
} },
// mounted() 内 buttonsClickFun 追加:
uploadCertificate: () => {
this.certificateOrderId = this.currentForm.id;
this.showCertificate = true;
}
模板末尾:
<el-dialog
title="上传凭证"
:visible.sync="showCertificate"
width="600px"
v-if="showCertificate"
:close-on-click-modal="false">
<out-certificate-form
:outOrderId="certificateOrderId"
@success="onCertificateSaved"
@cancel="showCertificate = false" />
</el-dialog>
onCertificateSaved() {
this.showCertificate = false;
this.$message.success('保存成功');
}
复用范围:本期只在「全部出库单」(
outAllDoc.vue/ web_id=211) 开放;「我的出库单」「待办出库单」暂不上。
4. 上线执行顺序
- DEV (
xiaoqu_franchisee_d+xiaoqu_comples_d)- 业务库执行 §1.1.1(建
warehouse_out_certificate) - 权限库执行 §1.2(按钮 + 授权,
USE xiaoqu_comples_d) - 后端编码 →
mvn -pl xiaoqu-publicbeanandmap install→mvn -pl xiaoqu-franchisee package→ 部署 Tomcat - 前端编码 →
yarn build-dev→ 部署
- 业务库执行 §1.1.1(建
- DEV 冒烟
- 管理员(authority=1)打开「全部出库单」→ 每行操作里出现「上传凭证」
- 点开 → 空态显示「暂无数据」 / 备注空白
- 上传 1 张 → ❌ → 提交 → 重新进入弹窗 → 凭证为空 ✅(验证"未提交不持久 / 提交才删")
- 上传 2 张 + 写备注 + 提交 → 重新进入 → 两张图回显 + 备注回显 ✅
- 改备注 + ❌ 掉 1 张 + 提交 → 重新进入 → 1 张图 + 新备注 ✅
- TEST (
xiaoqu_franchisee_d+test)- 业务表
warehouse_out_certificate已经存在于xiaoqu_franchisee_d(dev/test 共享业务库),无需再建 - 权限库执行 §1.2(按钮 + 授权,
USE test) - §1.2 第二段 INSERT 用动态 SELECT 自动复用 26 个 authority,不需要改 SQL
- 部署后端 / 前端到 test 环境
- 业务表
- PRD 上线前必查(仅校验,§1.2 SQL 不用改)
USE xiaoqu_comples_p; -- ① 「全部出库单」web_id 是否仍是 211 SELECT id, title FROM webpage WHERE path='warehouseInOutManage/outAllDoc'; -- ② 「全部出库单」当前授权范围(INSERT 后 button_webpage_authority 应等于这个数) SELECT COUNT(DISTINCT authority_id) AS total_authorities FROM webpage_authority WHERE web_id=211 AND soft_delete=0; -- ③ 防重复执行:确认 uploadCertificate 按钮尚未存在 SELECT id, web_id, en_name FROM button WHERE en_name='uploadCertificate' AND web_id=211; - PRD 上线:§1.2 SQL 把
USE改成USE xiaoqu_comples_p后整段照跑(不需要替换硬编码值,因为已动态化)+ 发包
5. 风险与注意
| 项 | 风险 | 处理 |
|---|---|---|
authority_id=1,102 硬编码 |
已用动态 SELECT 替换 | §1.2 INSERT button_webpage_authority 用子查询自动复用 webpage_authority 当前授权范围,dev/test/prod 通用 |
web_id=211 硬编码 |
各环境 webpage 主键大概率一致,但 prod 上线前仍要校验 | §4 ③ 校验脚本 |
| 跨环境 button.id 不同 | dev=789, test=790 是不同自增值 | SQL 用 LAST_INSERT_ID() 拿,前后端代码不耦合 button.id |
file_type=4 / in_out_allocation_type=99 是新枚举 |
与既有数据冲突 | 已查 dev:file_type 现仅 0-3、in_out_allocation_type 现仅 0-2/10-14,安全 |
/uploadFile/warehouseCertificate 是通用上传接口 |
不能在那里硬编码 file_type=4(会污染装机/送货流程) |
用"上传只落 pic 记录,aggregate_id=0;提交时再补 file_type/aggregate_id/type"两步法 |
| 用户上传后不提交导致孤儿图 | OSS 浪费 + DB 冗余 | §2.4 加定时清理(推荐);或留 backlog |
uk_out_order_id 唯一索引 |
同一出库单二次新增会冲突 | service 强制走 selectOne→update 路径,禁止裸 INSERT |
warehouse_in_out_pic.aggregate_id 默认 0 而非 NULL |
不能用 IS NULL 判孤儿 |
全程用 =0 判断(已在 §2.2.2 / §2.4 体现) |
| 文件数 / 大小限制仅前端 | 绕过前端直接调上传接口可超限 | 决策上接受;后续若发现滥用再补后端兜底 |
| PDF 预览 | 浏览器原生 PDF viewer 在内嵌 dialog 里可能体验差 | 用 window.open(url) 新标签页打开;图片走 el-image-viewer |
6. PM 确认结论(已敲定,2026-05-14)
| # | 决议 |
|---|---|
| 1 | 复用范围:本期仅 outAllDoc.vue(web_id=211)。我的出库 / 待办出库 暂不上。 |
| 2 | 列表角标:不做。砍掉 /certificate/exists 接口。 |
| 3 | 文件类型:图片 + PDF。复用现有 /uploadFile/warehouseCertificate(实测 OSSClientUtil.getContentType 已覆盖 jpg/png/gif/bmp/pdf 等 MIME,方法名 uploadImageStream2OSS 是历史遗留命名)。 |
| 4 | 数量/大小限制:最多 10 个、单个 ≤100MB,仅前端校验(before-upload 拦截)。 |
| 5 | "一键清空"按钮:不做。用户自行 ❌ 每个文件 + 清空备注 + 提交。 |