报价模板下线分类字段,匹配只靠业务类型与关键词;并补后台站点标题配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-15 14:31:52 +08:00
co-authored by Cursor
parent 0a3c74e3a1
commit 235ecd8c83
10 changed files with 45 additions and 117 deletions
@@ -34,7 +34,6 @@ class TemplateEntry:
version_id: str = ""
mapping_json: str = ""
biz_type: str = ""
category: str = ""
keywords: tuple[str, ...] = ()
is_default: bool = False
@@ -94,7 +93,6 @@ def entries_from_ledger_response(resp: LedgerResponse) -> list[TemplateEntry]:
version_id=str(it.get("versionId") or ""),
mapping_json=str(it.get("mappingJson") or ""),
biz_type=str(it.get("bizType") or ""),
category=str(it.get("category") or ""),
keywords=tuple(str(x) for x in (it.get("keywords") or []) if str(x).strip()),
is_default=bool(it.get("isDefault")),
)
@@ -155,7 +153,6 @@ def registry_as_dicts() -> list[dict[str, Any]]:
"template_id": e.template_id,
"version_no": e.version_no,
"biz_type": e.biz_type,
"category": e.category,
}
for e in list_template_registry()
]
@@ -50,11 +50,10 @@ public class InquiryQuoteTemplateController {
@GetMapping("/list")
public Result<IPage<Map<String, Object>>> list(@RequestParam(required = false) String keyword,
@RequestParam(required = false) String bizType,
@RequestParam(required = false) String category,
@RequestParam(required = false) String status,
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "20") Integer pageSize) {
return Result.OK(quoteTemplateService.pageWithVersion(keyword, bizType, category, status, pageNo, pageSize));
return Result.OK(quoteTemplateService.pageWithVersion(keyword, bizType, status, pageNo, pageSize));
}
@AutoLog(value = "报价模板-上传版本")
@@ -64,13 +63,12 @@ public class InquiryQuoteTemplateController {
public Result<Map<String, Object>> upload(@RequestParam(required = false) String templateId,
@RequestParam(required = false) String name,
@RequestParam(required = false) String bizType,
@RequestParam(required = false) String category,
@RequestParam(required = false) String keywordsJson,
@RequestParam(required = false, defaultValue = "auto") String mode,
@RequestParam(required = false, defaultValue = "false") Boolean isDefault,
@RequestParam("file") MultipartFile file) {
Map<String, Object> row = quoteTemplateService.uploadVersion(
templateId, name, bizType, category, keywordsJson, mode,
templateId, name, bizType, keywordsJson, mode,
Boolean.TRUE.equals(isDefault), file, currentUser());
return Result.OK(row);
}
@@ -136,7 +134,7 @@ public class InquiryQuoteTemplateController {
return Result.OK(quoteTemplateService.getById(id));
}
@ApiOperation("编辑逻辑头(名称/分类/关键词/模式,不换文件)")
@ApiOperation("编辑逻辑头(名称/业务/关键词/模式,不换文件)")
@RequiresPermissions("inquiry:template:edit")
@PutMapping("/editMeta")
public Result<String> editMeta(@RequestBody InquiryQuoteTemplate body) {
@@ -153,9 +151,6 @@ public class InquiryQuoteTemplateController {
if (StringUtils.isNotBlank(body.getBizType())) {
db.setBizType(body.getBizType());
}
if (StringUtils.isNotBlank(body.getCategory())) {
db.setCategory(body.getCategory());
}
if (StringUtils.isNotBlank(body.getKeywordsJson())) {
db.setKeywordsJson(body.getKeywordsJson());
}
@@ -17,7 +17,8 @@ import java.util.Date;
/**
* 报价模板逻辑头,对应 inq_quote_template。
* <p>
* 职责:业务身份(海陆空/分类/关键词/默认/启停/auto|manual)。
* 职责:业务身份(海陆空/关键词/默认/启停/auto|manual)。
* 分类字段已下线:匹配只靠 bizType + keywords + isDefault。
* 版式字节与映射在版本表;历史报价应绑 templateId + versionNo。
*/
@Data
@@ -38,9 +39,6 @@ public class InquiryQuoteTemplate implements Serializable {
@ApiModelProperty("海运/空运/陆运")
private String bizType;
@ApiModelProperty("分类")
private String category;
@ApiModelProperty("关键词 JSON 数组字符串")
private String keywordsJson;
@@ -15,13 +15,13 @@ import java.util.Map;
*/
public interface IInquiryQuoteTemplateService extends IService<InquiryQuoteTemplate> {
IPage<Map<String, Object>> pageWithVersion(String keyword, String bizType, String category, String status,
IPage<Map<String, Object>> pageWithVersion(String keyword, String bizType, String status,
int pageNo, int pageSize);
/**
* 新建逻辑模板并写入第一版;或对已有模板追加新版本。
*/
Map<String, Object> uploadVersion(String templateId, String name, String bizType, String category,
Map<String, Object> uploadVersion(String templateId, String name, String bizType,
String keywordsJson, String mode, boolean isDefault,
MultipartFile file, String operator);
@@ -57,20 +57,17 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
private String contractPath;
@Override
public IPage<Map<String, Object>> pageWithVersion(String keyword, String bizType, String category, String status,
public IPage<Map<String, Object>> pageWithVersion(String keyword, String bizType, String status,
int pageNo, int pageSize) {
QueryWrapper<InquiryQuoteTemplate> qw = new QueryWrapper<>();
if (StringUtils.isNotBlank(bizType)) {
qw.eq("biz_type", bizType);
}
if (StringUtils.isNotBlank(category)) {
qw.eq("category", category);
}
if (StringUtils.isNotBlank(status)) {
qw.eq("status", status);
}
if (StringUtils.isNotBlank(keyword)) {
qw.and(w -> w.like("name", keyword).or().like("category", keyword).or().like("keywords_json", keyword));
qw.and(w -> w.like("name", keyword).or().like("keywords_json", keyword));
}
qw.orderByDesc("update_time").orderByDesc("create_time");
IPage<InquiryQuoteTemplate> page = page(new Page<>(pageNo, pageSize), qw);
@@ -88,7 +85,6 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
m.put("id", t.getId());
m.put("name", t.getName());
m.put("bizType", t.getBizType());
m.put("category", t.getCategory());
m.put("keywords", parseKeywords(t.getKeywordsJson()));
m.put("mode", t.getMode());
m.put("status", t.getStatus());
@@ -122,7 +118,7 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> uploadVersion(String templateId, String name, String bizType, String category,
public Map<String, Object> uploadVersion(String templateId, String name, String bizType,
String keywordsJson, String mode, boolean isDefault,
MultipartFile file, String operator) {
if (file == null || file.isEmpty()) {
@@ -138,7 +134,6 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
template = new InquiryQuoteTemplate();
template.setName(StringUtils.defaultIfBlank(name, stripExt(original)));
template.setBizType(StringUtils.defaultIfBlank(bizType, "海运"));
template.setCategory(StringUtils.defaultIfBlank(category, defaultCategory(template.getBizType())));
template.setKeywordsJson(StringUtils.defaultIfBlank(keywordsJson, "[]"));
template.setMode(StringUtils.defaultIfBlank(mode, "auto"));
template.setStatus("disabled");
@@ -159,9 +154,6 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
if (StringUtils.isNotBlank(bizType)) {
template.setBizType(bizType);
}
if (StringUtils.isNotBlank(category)) {
template.setCategory(category);
}
if (StringUtils.isNotBlank(keywordsJson)) {
template.setKeywordsJson(keywordsJson);
}
@@ -445,7 +437,6 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
m.put("templateId", t.getId());
m.put("name", t.getName());
m.put("bizType", t.getBizType());
m.put("category", t.getCategory());
m.put("keywords", parseKeywords(t.getKeywordsJson()));
m.put("isDefault", t.getIsDefault() != null && t.getIsDefault() == 1);
m.put("versionNo", ver.getVersionNo());
@@ -716,14 +707,4 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
int i = name.lastIndexOf('.');
return i > 0 ? name.substring(0, i) : name;
}
private static String defaultCategory(String bizType) {
if ("空运".equals(bizType)) {
return "空运到港报价单模板";
}
if ("陆运".equals(bizType)) {
return "国内拼车";
}
return "海运报价单模板";
}
}
+6
View File
@@ -1,6 +1,12 @@
# 是否启用mock(生产必须 false,否则会走本地 Mock 像旧骨架)
VITE_USE_MOCK = false
# 网站标题(浏览器标签后缀;缺省会显示「页面名 undefined」)
VITE_GLOB_APP_TITLE = 友通达询价系统
# 简称,用于生产 _app.config.js 变量名;勿空格/数字开头
VITE_GLOB_APP_SHORT_NAME = YtdInquiryAdmin
# 发布路径
VITE_PUBLIC_PATH = /
+3 -1
View File
@@ -28,7 +28,9 @@ export function useTitle() {
}
const tTitle = t(route?.meta?.title as string);
pageTitle.value = tTitle ? ` ${tTitle} - ${title} ` : `${title}`;
// 生产 _app.config.js 若漏打 VITE_GLOB_APP_TITLE,title 会是 undefined → 标签出现「报价模板 undefined」
const appTitle = title || '友通达询价系统';
pageTitle.value = tTitle ? `${tTitle} - ${appTitle}` : `${appTitle}`;
},
{ immediate: true }
);
@@ -3,11 +3,14 @@
* placeholderCount 语义改为 mappingPointCount(映射点数)。
*/
/**
* 列表行类型。分类字段已从运营页移除(筛选项/列/表单均不展示);
* 主账库仍可存 category,由服务端按业务默认,前端不再读写。
*/
export type TemplateRow = {
id: string;
name: string;
bizType: "空运" | "海运" | "陆运";
category: string;
fileLabel: string;
fileName: string;
format: "excel" | "pdf";
@@ -28,34 +31,6 @@ export type TemplateRow = {
objectKey?: string;
};
/** 业务 → 分类(与 contracts/quote-template-mapping-v1.json 一致) */
export const CATEGORY_BY_BIZ: Record<TemplateRow["bizType"], string[]> = {
海运: ["海运报价单模板"],
空运: ["空运到港报价单模板", "空运到门报价单模板"],
陆运: [
"双清或中越",
"国内整车",
"国内拼车",
"跨境整车",
"跨境集拼",
"中港",
"凭祥保税仓&普仓仓储",
],
};
export const ALL_CATEGORIES = [
"空运到港报价单模板",
"空运到门报价单模板",
"海运报价单模板",
"双清或中越",
"国内整车",
"国内拼车",
"跨境整车",
"跨境集拼",
"中港",
"凭祥保税仓&普仓仓储",
];
/** 将主账 list 记录规范为页面行 */
export function mapApiRow(raw: any): TemplateRow {
const created =
@@ -68,7 +43,6 @@ export function mapApiRow(raw: any): TemplateRow {
id: String(raw.id || ""),
name: raw.name || "",
bizType: (raw.bizType || "海运") as TemplateRow["bizType"],
category: raw.category || "",
fileLabel: raw.fileLabel || raw.name || "",
fileName: raw.fileName || "",
format: raw.format === "pdf" ? "pdf" : "excel",
@@ -7,12 +7,7 @@
*/
import { computed, onMounted, reactive, ref } from "vue";
import { message } from "ant-design-vue";
import {
ALL_CATEGORIES,
CATEGORY_BY_BIZ,
mapApiRow,
type TemplateRow,
} from "/@/views/inquiry/mock/templates";
import { mapApiRow, type TemplateRow } from "/@/views/inquiry/mock/templates";
import ProtoIcon from "/@/views/inquiry/components/ProtoIcon.vue";
import {
disableTemplate,
@@ -47,7 +42,6 @@ const rows = ref<TemplateRow[]>([]);
const loading = ref(false);
const keyword = ref("");
const bizFilter = ref("");
const categoryFilter = ref("");
const statusFilter = ref("");
const formOpen = ref(false);
@@ -59,10 +53,10 @@ const previewVm = ref<PreviewViewModel>(emptyPreviewViewModel());
const fileInputRef = ref<HTMLInputElement | null>(null);
const pendingFile = ref<File | null>(null);
/** 运营表单不含「分类」:主账上传时按业务自行补默认分类 */
const form = reactive({
name: "",
bizType: "海运" as TemplateRow["bizType"],
category: "",
mode: "auto" as "auto" | "manual",
keywordItems: [""] as string[],
isDefault: false,
@@ -98,10 +92,6 @@ const previewTotalText = computed(() => {
return `${vm.currency || "USD"} ${amt}`;
});
function defaultCategoryForBiz(biz: TemplateRow["bizType"]) {
return (CATEGORY_BY_BIZ[biz] || [])[0] || "";
}
const filtered = computed(() => rows.value);
function bizTag(biz: string) {
@@ -110,13 +100,18 @@ function bizTag(biz: string) {
return "tag-blue";
}
/** 列表展示用:关键词用顿号拼接;空则显示占位 */
function formatKeywords(keywords: string[] | undefined) {
if (!keywords?.length) return "";
return keywords.map((k) => String(k || "").trim()).filter(Boolean).join("、");
}
async function loadList() {
loading.value = true;
try {
const res: any = await getTemplateList({
keyword: keyword.value.trim() || undefined,
bizType: bizFilter.value || undefined,
category: categoryFilter.value || undefined,
status: statusFilter.value || undefined,
pageNo: 1,
pageSize: 200,
@@ -139,7 +134,6 @@ function resetForm() {
Object.assign(form, {
name: "",
bizType: "海运" as TemplateRow["bizType"],
category: defaultCategoryForBiz("海运"),
mode: "auto" as const,
keywordItems: [""],
isDefault: false,
@@ -165,7 +159,6 @@ function openEdit(row: TemplateRow) {
Object.assign(form, {
name: row.name,
bizType: row.bizType,
category: row.category || defaultCategoryForBiz(row.bizType),
mode: row.mode || "auto",
keywordItems: row.keywords.length ? [...row.keywords] : [""],
isDefault: Boolean(row.isDefault),
@@ -318,8 +311,7 @@ async function submitForm() {
message.warning("请至少填写一个关键词");
return;
}
// 分类/模式不对运营展示:分类按业务默认,模式新建默认 auto,编辑保留原值
const category = defaultCategoryForBiz(form.bizType);
// 模式不对运营展示:新建默认 auto,编辑保留原值;分类交主账按业务补默认
const mode = editing.value?.mode || "auto";
try {
@@ -328,7 +320,6 @@ async function submitForm() {
if (editing.value?.id) fd.append("templateId", editing.value.id);
fd.append("name", form.name.trim());
fd.append("bizType", form.bizType);
fd.append("category", category);
fd.append("keywordsJson", JSON.stringify(keywords));
fd.append("mode", mode);
fd.append("isDefault", String(form.isDefault));
@@ -340,7 +331,6 @@ async function submitForm() {
id: editing.value.id,
name: form.name.trim(),
bizType: form.bizType,
category,
keywordsJson: JSON.stringify(keywords),
mode,
isDefault: form.isDefault ? 1 : 0,
@@ -398,10 +388,6 @@ async function downloadPreviewOrOriginal() {
<option value="海运">海运</option>
<option value="陆运">陆运</option>
</select>
<select v-model="categoryFilter" @change="loadList">
<option value="">全部分类</option>
<option v-for="c in ALL_CATEGORIES" :key="c" :value="c">{{ c }}</option>
</select>
<select v-model="statusFilter" @change="loadList">
<option value="">全部状态</option>
<option value="enabled">启用</option>
@@ -415,8 +401,7 @@ async function downloadPreviewOrOriginal() {
<tr>
<th>模板名称</th>
<th>业务</th>
<th>分类</th>
<th>模板文件</th>
<th>关键词</th>
<th>解析状态</th>
<th>状态</th>
<th>总使用次数</th>
@@ -434,8 +419,9 @@ async function downloadPreviewOrOriginal() {
<td>
<span class="tag" :class="bizTag(row.bizType)">{{ row.bizType }}</span>
</td>
<td class="qt-col-muted">{{ row.category }}</td>
<td class="qt-col-file">{{ row.fileLabel }}</td>
<td class="qt-col-keywords" :title="formatKeywords(row.keywords)">
{{ formatKeywords(row.keywords) || "—" }}
</td>
<td class="qt-col-muted">{{ row.parseStatus }}</td>
<td class="qt-col-muted">{{ row.status === "enabled" ? "启用" : "禁用" }}</td>
<td class="qt-col-count">{{ row.useCount }} 次</td>
@@ -732,10 +718,14 @@ async function downloadPreviewOrOriginal() {
font-size: 11px;
color: #334155;
}
.qt-col-file {
.qt-col-keywords {
font-size: 11px;
font-weight: 700;
font-weight: 600;
color: #334155;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.qt-col-count {
font-size: 11px;
+3 -18
View File
@@ -5,7 +5,7 @@
职责:
1) 扫描仓库「海陆空报价单模板」目录(忽略 ~$ 临时文件与嵌套重复)
2) 按文件名判定 bizType / category / mode(auto|manual) / 关键词
2) 按文件名判定 bizType / mode(auto|manual) / 关键词
3) 用 openpyxl 做标签粗映射,写出 seed 清单 JSON(供人工核对)
4) 若提供 --upload-url 与 token,则逐个 POST 到主账 /inquiry/quoteTemplate/upload
@@ -32,7 +32,7 @@ OUT_JSON = ROOT / "deploy" / "seed" / "quote_templates_seed_manifest_v1.json"
def classify(name: str) -> dict[str, Any]:
"""按文件名归类业务/分类/模式。"""
"""按文件名归类业务/模式/关键词(分类字段已下线)。"""
n = name
mode = "auto"
if any(k in n for k in ("人工报价", "双清", "保税仓", "仓储")):
@@ -41,82 +41,69 @@ def classify(name: str) -> dict[str, Any]:
if "海运" in n:
return {
"bizType": "海运",
"category": "海运报价单模板",
"keywords": ["海运", "整柜", "FCL"],
"mode": mode,
}
if "空运到门" in n:
return {
"bizType": "空运",
"category": "空运到门报价单模板",
"keywords": ["空运", "到门"],
"mode": mode,
}
if "空运" in n:
return {
"bizType": "空运",
"category": "空运到港报价单模板",
"keywords": ["空运", "到港"],
"mode": mode,
}
if "中港" in n:
return {
"bizType": "陆运",
"category": "中港",
"keywords": ["中港", "陆运"],
"mode": mode,
}
if "拼车" in n:
return {
"bizType": "陆运",
"category": "国内拼车",
"keywords": ["拼车", "零担"],
"mode": mode,
}
if "国内" in n and "整车" in n:
return {
"bizType": "陆运",
"category": "国内整车",
"keywords": ["整车", "国内"],
"mode": mode,
}
if "跨境整车" in n:
return {
"bizType": "陆运",
"category": "跨境整车",
"keywords": ["跨境", "整车"],
"mode": mode,
}
if "跨境集拼" in n or "中越" in n or "集拼" in n:
cat = "双清或中越" if ("中越" in n or "双清" in n) else "跨境集拼"
return {
"bizType": "陆运",
"category": cat,
"keywords": ["集拼", "跨境"] if "集拼" in n else ["中越"],
"mode": mode,
}
if "凭祥" in n or "仓储" in n:
return {
"bizType": "陆运",
"category": "凭祥保税仓&普仓仓储",
"keywords": ["仓储", "凭祥"],
"mode": "manual",
}
if "双清" in n:
return {
"bizType": "陆运",
"category": "双清或中越",
"keywords": ["双清"],
"mode": "manual",
}
return {
"bizType": "陆运",
"category": "国内拼车",
"keywords": [Path(name).stem[:20]],
"mode": mode,
}
def unique_xlsx_files() -> list[Path]:
files = [p for p in TEMPLATE_DIR.rglob("*.xlsx") if not p.name.startswith("~$")]
by_name: dict[str, Path] = {}
@@ -181,7 +168,6 @@ def build_manifest() -> list[dict[str, Any]]:
"fileName": path.name,
"relativePath": str(path.relative_to(ROOT)).replace("\\", "/"),
"bizType": meta["bizType"],
"category": meta["category"],
"keywords": meta["keywords"],
"mode": meta["mode"],
"suggestedMappingPointCount": mapped.get("mappingPointCount", 0),
@@ -201,7 +187,6 @@ def upload_one(base_url: str, token: str, item: dict[str, Any]) -> dict[str, Any
fields = {
"name": Path(item["fileName"]).stem,
"bizType": item["bizType"],
"category": item["category"],
"keywordsJson": json.dumps(item["keywords"], ensure_ascii=False),
"mode": item["mode"],
"isDefault": "false",
@@ -259,7 +244,7 @@ def main(argv: list[str]) -> int:
print(f"wrote {OUT_JSON} count={len(items)}")
for it in items:
print(
f" [{it['mode']}] {it['bizType']}/{it['category']} "
f" [{it['mode']}] {it['bizType']} "
f"map~{it['suggestedMappingPointCount']} {it['fileName']}"
)