diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..0241e51
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,9 @@
+.git
+.playwright-cli
+.impeccable
+dist
+node_modules
+node_modules.broken-by-path
+.env.local
+npm-debug.log*
+pnpm-debug.log*
diff --git a/.env.example b/.env.example
index 17b31d0..6144c14 100644
--- a/.env.example
+++ b/.env.example
@@ -1,2 +1,5 @@
VITE_API_BASE_URL=/api
+VITE_OVERSEAS_API_BASE_URL=/overseas-api
+OVERSEAS_API_PROXY_TARGET=http://127.0.0.1:9300
API_PROXY_TARGET=http://127.0.0.1:9000
+VITE_ADMIN_WEB_ORIGIN=http://localhost:5173
diff --git a/.gitignore b/.gitignore
index e2bc4c3..9b7adcb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
node_modules/
node_modules.broken-by-path/
dist/
+tmp/
.env
.env.*
@@ -16,4 +17,3 @@ outputs/
*.log
.DS_Store
Thumbs.db
-
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/client-web.iml b/.idea/client-web.iml
new file mode 100644
index 0000000..d6ebd48
--- /dev/null
+++ b/.idea/client-web.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..07115cd
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..e8c26cd
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
index a7f2244..ce0f91d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -3,7 +3,8 @@
## 项目定位
- 本项目是入驻企业及其成员使用的客户工作台,所有业务数据都处于企业租户上下文中。
-- 当前范围包括注册、业务总览、企业资料、企业成员、企业角色与权限、头程订单。
+- 当前范围包括注册、业务总览、企业资料、企业成员、企业角色与权限、头程订单、客户资金账户、客户财务账单查询与客户确认,以及海外仓贴标、转运、废弃和退货换标自助服务。
+- 用户可见的“平台—客户”层级语义读取 `../../docs/平台客户层级与权限契约.md`;涉及海外 API、海外 Token 或跨区域事件时读取 `../../docs/跨区域认证与服务通信契约.md`。本文件中的 `tenant` 仅表示服务端隔离上下文,不扩展用户可见产品层级。
- 不在本项目实现平台内部运营管理、品牌官网、仓库 PDA 作业或仅属于原生移动端的能力。
## 技术基线
@@ -16,30 +17,72 @@
## 目录与职责
-- `src/views/`:路由级页面和业务流程入口;复用交互拆入组件。
+- `src/modules/`:按业务域纵向组织页面、路由、业务组件、API、store、类型、数据适配和业务工具。新增业务页面必须进入对应模块,不再在 `src/views/` 堆放。
+- `src/modules//routes.ts`:作为模块 manifest 同时声明页面路由、导航元数据和发布状态;`src/modules/registry.ts` 统一组装模块,`src/router/index.ts` 只负责应用壳层和全局守卫。
+- manifest 中 `release: 'active'` 的页面才会进入路由和导航;占位页面必须标记为 `release: 'prototype'`,保留源码但不可通过 URL 访问。接口、权限资源和产品范围确认后才能改为 `active`。
+- `src/components/common/`:跨两个及以上业务模块实际复用的通用组件;仅被一个模块使用的组件保留在该模块内。
- `src/layouts/`:租户工作台壳层、导航和全局页面结构。
- `src/router/`:公开页、受保护路由、页面标题和权限元数据;导航与路由必须同步。
-- `src/stores/`:认证、当前租户和跨页面业务状态;局部表单状态留在页面或组件内。
-- `src/api/`:统一处理基础地址、超时、认证头、响应解包和错误映射;页面不得直接创建独立 Axios 客户端。
-- `VITE_API_BASE_URL` 控制浏览器请求前缀,`API_PROXY_TARGET` 控制 Vite 开发代理上游;后者只由开发服务器读取,不进入客户端构建。新增或变更环境变量时同步更新 `.env.example`,不得提交真实密钥或环境专属凭据。
+- 模块内 `store/`:该业务跨页面状态;局部表单状态仍留在页面或组件内。跨模块使用 store 时只能导入其公开 store 文件,不得读取模块内部页面或组件实现。
+- `src/api/`:只保留 HTTP、会话令牌、响应类型等全局请求基础设施;业务接口放入对应模块的 `api/`。页面不得直接创建独立 Axios 客户端。
+- 模块之间不得通过相对路径或内部目录穿透;确需协作时只能从目标模块的 `public.ts` 导入。模块内稳定契约放在 `contracts.ts`,稳定的跨应用模型仍应上移到 `@chongchong/shared`。
+- `VITE_API_BASE_URL`、`VITE_OVERSEAS_API_BASE_URL` 分别控制国内和海外浏览器前缀;`API_PROXY_TARGET`、`OVERSEAS_API_PROXY_TARGET` 只由 Vite 开发代理读取,不进入客户端构建。新增或变更环境变量时同步更新 `.env.example`,不得提交真实密钥或环境专属凭据。
- `src/styles/`:全局设计令牌和布局;优先复用现有 `--primary`、`--navy`、`--border`、`--muted`。
-- 跨应用稳定模型放到 `packages/shared`,客户工作台专属类型保留在本项目。
+- 本应用多个模块稳定复用的前端模型放到应用内 `shared/`,客户工作台专属类型保留在对应业务模块。不要链接应用目录之外的 TypeScript 包,服务端契约以 `baiyue-firstleg` Controller/OpenAPI 为准。
+
+## 开发环境样式验收
+
+- 开发环境验收使用本轮动态创建并已审核通过的客户管理员,或由用户显式提供的客户账号,不依赖固定业务客户或固定账号。
+- 账号与密码不写入仓库;从本机未提交环境变量或 `C:/Users/BY-OY/.codex/chongchong-global-dev-acceptance.env` 读取。若显式验收凭据不存在,再向用户索取。
+- 该身份仅适用于开发环境。不要把密码、Token 或真实接口地址写入源码、测试快照、日志或 `AGENTS.md`;开发环境变更时只更新本机凭据。
+- 由人工使用该身份进入真实页面,按 `1920×1080` 视口和 `100%` 浏览器缩放完成操作与截图,再把截图发送给 AI 核对加载、空态、错误态、权限态和桌面布局;AI 默认不主动调用截图或视觉类技能取证。
+
+## 模块快速查询
+
+退货预报与签收结果:`src/modules/overseas-warehouse/views/ReturnOrdersView.vue`、`api/returnOrders.ts`;菜单仍名为“退货换标”,正式路由 `return-relabel` 已使用独立页面,不再由 `WarehouseServiceView.vue` 承载。客户退货库存/箱号共用 `views/ReturnInventoryView.vue`、`api/returnInventory.ts`,`return-cartons` 传 `cartonOnly=true`。Backend 为 Lastleg `returnreceipt/`,退货单签收完成不等于换标或退货出库完成;后续换标、出库须按各自业务状态核对。
+
+先从 `src/modules/registry.ts` 确认模块,再按目标模块的 `routes.ts → views/ → api/、store/、contracts.ts 或 public.ts` 查询。页面是否正式开放以 `routes.ts` 的 `release` 为准,不从任务或进度文档推断。
+
+| 业务 | 模块入口 | 联查模块或服务端 |
+|---|---|---|
+| 登录、注册与会话 | `src/modules/auth/`、`src/api/session.ts`、`src/api/http.ts` | Backend `auth/`、`iam/`;正常旋转的国内/海外 Token 会话统一持久化,“记住账号”只记登录标识;访问令牌失效但刷新令牌仍在时先恢复会话,公开认证接口不得携带旧 Token |
+| 企业资料、成员与角色 | `src/modules/tenant-management/`、`src/modules/account/` | Backend `iam/`、`auth/`、`file/`;公司联系电话/邮箱是企业资料,不作为登录标识;成员登录手机号可选,修改时同步 Auth、撤销该账号会话并审计。审核必填资质仅营业执照与统一社会信用代码;法人身份证选填,可随营业执照提交,也可在已有营业执照后单独补传或替换。前端角色树到服务端权限的映射统一维护在 `contracts.ts` 的 `FRONTEND_TO_MERCHANT_PERMISSION_CODES`,海外入仓、采购、本土卡车和散板权限必须显式进入该映射 |
+| 工作台与系统入口 | `src/modules/dashboard/`、`src/modules/system/` | `src/navigation/clientNavigation.ts`、`src/router/index.ts` |
+| 企业成员消息收件箱 | `src/modules/notifications/` | Backend `iam/src/main/java/com/baiyue/iam/notification/`;列表、详情、已读与未读数契约见 `iam/src/main/resources/openapi/notification-v1.yaml` |
+| 头程下单、中文批量导入、入仓码关系、箱唛、履约进度与入库费用确认 | `src/modules/first-leg/`;列表与批量入口 `views/FirstLegOrdersView.vue`,模板/导入 API `api/firstLeg.ts`,入仓详情 `components/InboundCodeAssociationDialog.vue`、`api/inboundCodeAssociations.ts`,独立计费重弹框 `components/InboundBillingPanel.vue`、`api/inboundBilling.ts` | 中文 XLSX 模板按当前客户生成地址、全部有效仓库和渠道下拉,最多 50 行,只创建草稿并支持部分/原子导入;导入结果只显示 Excel 行号,不展示内部 `clientRowId`。所有订单行均可进入计费重弹框,尚未推送时展示明确空态;只有具备编辑权限且服务端返回当前 `SENT` 费用时才可二次确认并提交 `expectedFeeVersion` 与幂等键,成功后版本锁定、平台不得再修改。Backend `order/`、`firstleg/`、`warehouse/`,不得用内部 ID 补齐缺失业务编号 |
+| 装柜完成后的退税报关资料 | `src/modules/first-leg/views/FirstLegOrdersView.vue` 的“报关资料”、`components/CustomsDocumentsDialog.vue`、`api/customs.ts` | 只显示登录客户在同柜的退税入仓码;报关开始前可上传、替换、调整同一资料覆盖的一个或多个入仓码及删除误传,全部写入携带幂等键和版本;开始后只读冻结资料,整柜完成后才能下载平台回传的一对一完成文件。Backend `firstleg/customs/container/`、`customs-v1.yaml`,二进制归 `file/` |
+| 统一订单展示 | `src/modules/order-management/` | `src/modules/first-leg/` 与对应后端订单契约 |
+| 客户财务账单、入仓码联查、收款、未收余额与客户确认 | `src/modules/finance/`;确认与明细入口 `views/FinanceBillsView.vue`、`api/finance.ts`,入仓码通过 `src/modules/first-leg/public.ts` 复用 `InboundCodeAssociationDialog` | Backend `finance/`;账单行只显示服务端 `inboundCode` 并按登录客户查询业务关系,缺失显示“—”,不得回退内部 ID。收款方式由服务端枚举映射中文。出账或已出账调整后为 `PENDING_CONFIRMATION`,客户确认提交登录成员、`expectedVersion` 和幂等键 |
+| 客户资金账户、授信与充值申请 | `src/modules/finance/views/FinanceAccountView.vue`、`api/account.ts` | 显示当前企业 CNY 实际余额、冻结金额、授信额度、授信有效期、已用授信、冻结授信、总可用余额和不可变流水;数据加载前显示“—”,不得先伪装为零。授信到期日当天仍有效,次日已用授信并入实际余额,负数实际余额会阻止提交新订单。充值只展示服务端启用的微信/支付宝静态收款码,客户上传 JPG/PNG 付款截图后进入待审核。客户范围只取登录态,无收款码时禁止提交,不模拟支付成功 |
+| 海外采购、美国本土卡与美本土散板 | `src/modules/cross-border/routes.ts`、`components/BusinessOrdersView.vue`、`api/businessOrders.ts` | 三个路由均为 `active`,客户范围只取 Token;采购创建的 `customerName` 只取当前认证会话企业名称,不允许自由输入或冒用其他客户。Backend `lastleg-service/` 的 `procurement/`、`trucking/`、`looseboard/` 客户接口 |
+| 海外仓入库订单 | `src/modules/order-management/routes.ts`、`views/InboundOrdersView.vue`、`api/inboundPlans.ts` | `orders/inbound` 为只读列表/详情,权限 `merchant:warehouse:inbound:list`;不展示或提交内部 ID |
+| 海外仓贴标、仓储转运、货物废弃 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 三个路由均为 `active`;退货预报另见本节独立入口;贴标申请先选国外中转仓和有库存历史入仓单,换标申请必须提交正数 CNY `chargeAmount`,换标类型为只换 SKU、只换箱唛、SKU+箱唛。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 1。货物明细外层始终提供“SKU 关联”“箱号关联”“SKU 混箱换标”三套模板的下载、上传,不随换标类型切换;上传只进入待导入关系区,客户可勾选部分或全选后导入本次明细,只有进入明细的库存才随申请提交和冻结。明细也支持手动新增编辑,组合换标以旧箱唛连接旧/新 SKU 和旧箱唛/新 FBA 箱唛关系。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID |
## 租户、认证与数据安全
+- 换标申请必须提交正数 CNY `chargeAmount`;入口为 `src/modules/overseas-warehouse/views/WarehouseServiceView.vue` 与 `api/warehouseServices.ts`,金额由服务端再次校验,资金状态以服务端返回为准,不在客户端模拟冻结成功。
+
- 任何查询和变更都必须以服务端确认的当前租户为边界;绝不从可篡改的前端参数推断授权租户。
- 企业成员、角色和权限只能管理当前企业范围。前端隐藏不是安全控制,服务端必须重复校验租户与权限。
-- 认证统一经过 `src/stores/auth.ts` 和 `src/api/http.ts`;不要在页面中复制令牌逻辑或持久化密码、敏感资质内容。
+- 注册时登录手机号同时初始化公司联系电话;注册完成后两者独立维护。企业资料只修改 `contactPhone`,个人资料或成员管理中的登录手机号由 IAM/Auth 同步并撤销该账号现有会话,不得在前端互相回填或联动提交。
+- 认证统一经过 `src/modules/auth/store/auth.ts`、`src/api/http.ts`、`src/api/session.ts`;国内请求使用国内 Access Token,海外请求统一由 `src/api/overseasHttp.ts` 读取短时海外 Token。启动时 Access Token 已过期但 Refresh Token 仍存在,必须先尝试恢复会话;并发或跨标签页 401 共用一次刷新编排,每个原请求最多重放一次,只有明确 401/会话失效才清理 Token 并保留原路径跳转登录页。登录页“记住账号”只保存登录 ID,不改变 Access/Refresh Token 的持久化与续期策略;页面不要复制令牌逻辑或持久化密码、敏感资质内容。
- 当前认证仍包含模拟数据。接入真实 API 后移除对应模拟分支,生产环境不得回退为模拟成功。
+- 头程订单统一使用当前正式读写契约;不得继续扩展 `LEGACY_MANUAL`、旧系统入仓码、C02 不可用回退旧 API、历史部分响应兼容或其他新旧并跑逻辑。触及该链路时按根无兼容规则同步删除生产分支、类型、文案、Mock 和测试,不以现有代码或测试仍覆盖旧路径作为保留依据。
- 注册、上传资质、成员邀请和订单操作必须提供明确校验、幂等保护、提交中状态和可恢复的失败反馈。
## UI 与交互
+- 修改已有 Client 原型页面前先读 `../../docs/Admin-Client前端原型还原验收规则.md`,按页面和操作形成原型核对矩阵;本节维护 Client 的实现约束,不复制跨前端验收文档全文。
+- 已向客户讲解或确认过的产品原型是页面视觉与布局实现的直接事实来源。字段顺序、字段分组、字段摆放、列宽、控件宽高、对齐方式、换行方式、区块层级、上下左右边距、内边距、间隙、留白、圆角、边框、阴影、背景色、文字颜色和字号均必须按原型还原,不得只参考大致风格后自行重新排版。
+- 原型中的按钮位置、顺序、尺寸、圆角、边框、背景色、文字色、图标、悬停态、激活态、禁用态和危险语义必须直接按原型实现;不得擅自改为文字链接、不同颜色、不同按钮类型、折叠菜单或自创交互效果,也不得增加原型没有的上浮、缩放、渐变线、装饰图标或其他视觉元素。
+- 原型已经给出明确图标、字符图标、图片资源或按钮内图标时,直接使用原型中的同一方案和资源,不得以 Lucide、Element Plus 默认图标、手绘图标或个人偏好替换。仅当原型没有定义对应图标时,才按本节的默认图标语义补齐。
+- 上述原型还原规则优先于本文件中“延续现有视觉体系”、默认组件样式和默认图标库等一般性规则。只有用户当前明确要求、阶段范围、真实字段长度、响应式、可访问性或接口事实与原型冲突时才允许做最小调整,并须在修改前说明具体差异、原因和影响范围;不得以技术实现方便为由偏离原型。
+- 详情、新增、编辑、审核等操作采用独立页面、居中弹框、侧滑面板或其他承载方式,必须逐页面按对应原型确认,不得全局写死为某一种容器。原型为独立页面时使用正式路由和页面布局;原型为居中弹框时使用模态弹框;只有原型明确为侧滑面板时才使用抽屉。不得因为已有公共组件或改动方便,把原型中的独立页面改成弹框、把弹框改成抽屉,或反向替换。
- 延续现有客户工作台的可信、简洁和服务型视觉语言,与内部管理后台保持品牌关联但不照搬高密度管理界面。
- 优先使用 Element Plus 和 Lucide 图标;不使用表情符号代替功能图标,不引入第二套组件库。
- 业务页按实际场景覆盖适用的加载、空数据、失败、无权限和数据过期状态;危险操作需要确认和明确结果。
- 保持键盘可达、可见焦点、语义化标签、足够对比度和清晰错误提示。
-- 响应式变更至少检查 320px、720px 和桌面布局;避免只能横向滚动才能完成核心任务。
+- 当前样式开发与验收只处理 `1920×1080` 桌面视口,不检查或整改 `720px`、`320px` 及其他窄屏;这些视口不作为测试、截图或交付门禁。保留现有响应式实现,不为当前阶段主动删除,用户恢复多视口验收后再统一处理。
## 本地命令与质量门禁
@@ -56,3 +99,21 @@ pnpm build
- 日常变更至少运行 `pnpm typecheck`;交付前运行 `pnpm build`。
- 当前测试使用 Node.js 内置测试运行器,执行 `pnpm test`;项目仍未声明 lint 脚本,不得声称 lint 已通过。
- 不手工修改 `dist/` 或 `node_modules/`;依赖变更必须同步当前项目依赖清单和锁文件。
+
+## 全量验收脚本
+
+- Client 全量验收唯一入口为 `../../baiyue-firstleg/scripts/client-test-env-acceptance.mjs`;入库测量与计费专项场景才单独执行 `client-phase2-test-env-acceptance.mjs`,不替代完整入口。
+- 入口通过测试环境 HTTP 验证注册/审核、客户隔离、订单、入库计费、文件、财务和权限链路,会创建或修改测试数据,必须获得当前对话明确授权。
+- `pnpm test/typecheck` 和 `frontend-full-regression.ps1` 仅是本地质量检查,不代表 Client 服务已完成全量验收。
+- HTTP 验收通过后,由人工使用开发验收账号逐页检查真实接口调用、客户可见字段、原型、权限、错误态、空态和 `1920×1080` 桌面布局,并将截图发送给 AI 分析;未收到人工截图时标记为待人工确认,不追加 AI 截图或窄屏浏览器验收。
+
+## Client 全量验收快速索引
+
+当前按客户侧全量业务闭环验收。业务域文档用于核对功能来源和验收证据,不限制本次验收的模块范围;按“模块 `routes.ts` → 页面/组件 → `api`、store、contracts”定位。本表只描述 Client 页面与客户可见契约,服务端实现详见后端模块指引。
+
+| 验收域 | Client 范围与代码入口 | 验收首读 |
+|---|---|---|
+| 客户基础与订单 | 登录/注册、企业资料与资质、成员/角色权限、工作台和订单 | `src/modules/auth/`、`tenant-management/`、`account/`、`system/`、`dashboard/`、`first-leg/`、`order-management/`;按客户可见契约核对权限、状态、幂等和错误处理 |
+| 履约、海外仓与财务 | 入库计费、柜次进度、船期、海外仓贴标/转运/废弃/退货换标、消息和客户账单 | `src/modules/first-leg/`、`overseas-warehouse/`、`finance/`、`notifications/`;按服务端权限核对客户可见字段、客户隔离和只读/写入边界 |
+
+客户侧边界:仍以服务端权限和客户可见字段为准;PDA、平台后台操作和内部证据不因全量验收自动暴露到 Client。财务按 `../../docs/财务管理/` 与 `src/modules/finance/routes.ts` 定位。
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..77cd790
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,32 @@
+# 构建 Vue 静态文件。CI 通过构建参数切换到内网基础镜像,
+# 本地开发仍可使用 Docker Hub 默认值。
+ARG NODE_IMAGE=node:22-alpine
+ARG NGINX_IMAGE=nginx:1.27-alpine
+FROM ${NODE_IMAGE} AS build
+
+WORKDIR /app
+
+# Corepack 与 pnpm 依赖均通过 npmmirror 下载,避免构建时访问 npm 官方源过慢。
+ENV COREPACK_NPM_REGISTRY=https://registry.npmmirror.com
+ENV NPM_CONFIG_REGISTRY=https://registry.npmmirror.com
+RUN corepack enable
+
+COPY package.json pnpm-lock.yaml ./
+COPY shared/package.json ./shared/package.json
+
+# package.json 和 lock 文件未变化时,这一层会由 Docker 自动复用。
+RUN pnpm config set registry https://registry.npmmirror.com && \
+ pnpm install --frozen-lockfile
+
+COPY . .
+ARG VITE_ADMIN_WEB_ORIGIN
+ENV VITE_ADMIN_WEB_ORIGIN=${VITE_ADMIN_WEB_ORIGIN}
+RUN pnpm build
+
+# 运行阶段仅保留构建产物与 Nginx。
+FROM ${NGINX_IMAGE}
+
+COPY nginx/default.conf.template /etc/nginx/templates/default.conf.template
+COPY --from=build /app/dist /usr/share/nginx/html
+
+EXPOSE 80
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..3b003a9
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,176 @@
+pipeline {
+ agent {
+ label "${env.JOB_BASE_NAME?.endsWith('-prod') ? 'java-builder' : (env.JOB_BASE_NAME?.endsWith('-dev') ? 'java-dev' : 'java-test')}"
+ }
+
+ environment {
+ DEVELOPMENT_COMPOSE_FILE = 'docker-compose.development.yml'
+ DEVELOPMENT_COMPOSE_PROJECT = 'client-web'
+ REGISTRY_HOST = '172.31.198.7:10082'
+ REGISTRY_CREDENTIALS = 'gitea-registry-token'
+ NODE_BASE_IMAGE = '172.31.198.7:10082/java-project/node:22-alpine'
+ NGINX_BASE_IMAGE = '172.31.198.7:10082/java-project/nginx:1.27-alpine'
+ PRODUCTION_IMAGE_REPOSITORY = 'git.buggoglobal.com/html/client-web'
+ }
+
+ options {
+ timestamps()
+ disableConcurrentBuilds()
+ }
+
+ stages {
+ stage('确认提交版本') {
+ steps {
+ script {
+ env.IS_DEVELOPMENT = env.JOB_BASE_NAME?.endsWith('-dev') ? 'true' : 'false'
+ env.IS_PRODUCTION = env.JOB_BASE_NAME?.endsWith('-prod') ? 'true' : 'false'
+ if (env.IS_PRODUCTION == 'true') {
+ sh '''
+ set -eu
+ git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
+ test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" || {
+ echo "生产部署只允许 origin/main 当前提交;请先合并代码到 main" >&2
+ exit 1
+ }
+ '''
+ }
+ env.SOURCE_TAG = sh(script: 'git rev-parse --short=12 HEAD', returnStdout: true).trim()
+ }
+ sh 'git log -1 --oneline'
+ }
+ }
+
+ stage('构建并部署测试前端') {
+ when { expression { env.IS_DEVELOPMENT != 'true' && env.IS_PRODUCTION != 'true' } }
+ steps {
+ withCredentials([usernamePassword(credentialsId: env.REGISTRY_CREDENTIALS, usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASSWORD')]) {
+ sh '''
+ set -eu
+ export DOCKER_CONFIG="${WORKSPACE}@tmp/docker-auth"
+ rm -rf "$DOCKER_CONFIG"
+ mkdir -p "$DOCKER_CONFIG"
+ cleanup_registry() {
+ docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true
+ rm -rf "$DOCKER_CONFIG"
+ }
+ trap cleanup_registry EXIT HUP INT TERM
+ printf %s "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin "$REGISTRY_HOST"
+ docker compose -f docker-compose.test.yml build --pull=false
+ docker compose -f docker-compose.test.yml up -d --remove-orphans
+ '''
+ }
+ }
+ }
+
+ stage('构建并发布生产镜像') {
+ when { expression { env.IS_PRODUCTION == 'true' } }
+ steps {
+ withCredentials([usernamePassword(credentialsId: env.REGISTRY_CREDENTIALS, usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASSWORD')]) {
+ sh '''
+ set -eu
+ export DOCKER_CONFIG="${WORKSPACE}@tmp/docker-auth"
+ rm -rf "$DOCKER_CONFIG"
+ mkdir -p "$DOCKER_CONFIG"
+ cleanup_registry() {
+ docker logout git.buggoglobal.com >/dev/null 2>&1 || true
+ rm -rf "$DOCKER_CONFIG"
+ }
+ trap cleanup_registry EXIT HUP INT TERM
+ printf %s "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin git.buggoglobal.com
+ image="$PRODUCTION_IMAGE_REPOSITORY:$SOURCE_TAG"
+ docker build \
+ --build-arg NODE_IMAGE="$NODE_BASE_IMAGE" \
+ --build-arg NGINX_IMAGE="$NGINX_BASE_IMAGE" \
+ --build-arg VITE_ADMIN_WEB_ORIGIN=https://admin.buggoglobal.com \
+ -t "$image" .
+ docker push "$image"
+ '''
+ }
+ }
+ }
+
+ stage('确认部署生产前端') {
+ when { expression { env.IS_PRODUCTION == 'true' } }
+ steps {
+ input message: "确认将 main 提交 ${env.SOURCE_TAG} 部署到 app.buggoglobal.com?", ok: '确认部署'
+ }
+ }
+
+ stage('部署生产前端') {
+ when { expression { env.IS_PRODUCTION == 'true' } }
+ steps {
+ lock(resource: 'firstleg-production-client-web-deploy', inversePrecedence: true) {
+ withCredentials([
+ usernamePassword(credentialsId: 'gitea-production-pull-token', usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASSWORD'),
+ sshUserPrivateKey(credentialsId: 'firstleg-production-ssh', keyFileVariable: 'TARGET_SSH_KEY', usernameVariable: 'TARGET_SSH_USER'),
+ string(credentialsId: 'firstleg-production-host', variable: 'TARGET_SSH_HOST'),
+ file(credentialsId: 'firstleg-production-known-hosts', variable: 'TARGET_SSH_KNOWN_HOSTS')
+ ]) {
+ withEnv([
+ 'TARGET_DEPLOY_DIR=/data/firstleg/deploy/frontend',
+ 'TARGET_SSH_TARGET=' + env.TARGET_SSH_USER + '@' + env.TARGET_SSH_HOST
+ ]) {
+ sh "sh scripts/deploy-production.sh client-web '${env.PRODUCTION_IMAGE_REPOSITORY}' '${env.SOURCE_TAG}' docker-compose.production.yml"
+ }
+ }
+ }
+ }
+ }
+
+ stage('构建并部署开发前端') {
+ when { expression { env.IS_DEVELOPMENT == 'true' } }
+ steps {
+ withCredentials([usernamePassword(credentialsId: env.REGISTRY_CREDENTIALS, usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASSWORD')]) {
+ sh '''
+ set -eu
+ export DOCKER_CONFIG="${WORKSPACE}@tmp/docker-auth"
+ rm -rf "$DOCKER_CONFIG"
+ mkdir -p "$DOCKER_CONFIG"
+ cleanup_registry() {
+ docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true
+ rm -rf "$DOCKER_CONFIG"
+ }
+ trap cleanup_registry EXIT HUP INT TERM
+ printf %s "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin "$REGISTRY_HOST"
+ image="client-web-development:${SOURCE_TAG}"
+
+ docker build \
+ --build-arg NODE_IMAGE="$NODE_BASE_IMAGE" \
+ --build-arg NGINX_IMAGE="$NGINX_BASE_IMAGE" \
+ --build-arg VITE_ADMIN_WEB_ORIGIN=http://8.163.49.32:9001 \
+ -t "$image" .
+ export IMAGE_TAG="$SOURCE_TAG"
+ docker compose -p "$DEVELOPMENT_COMPOSE_PROJECT" -f "$DEVELOPMENT_COMPOSE_FILE" up -d --remove-orphans --pull never
+ docker compose -p "$DEVELOPMENT_COMPOSE_PROJECT" -f "$DEVELOPMENT_COMPOSE_FILE" ps
+ docker compose -p "$DEVELOPMENT_COMPOSE_PROJECT" -f "$DEVELOPMENT_COMPOSE_FILE" exec -T client-web wget -qO- http://127.0.0.1/healthz
+ '''
+ }
+ }
+ }
+
+ stage('部署检查') {
+ when { expression { env.IS_DEVELOPMENT != 'true' && env.IS_PRODUCTION != 'true' } }
+ steps {
+ sh '''
+ set -eu
+ docker compose -f docker-compose.test.yml ps
+ docker compose -f docker-compose.test.yml exec -T client-web wget -qO- http://127.0.0.1/healthz
+ '''
+ }
+ }
+ }
+
+ post {
+ failure {
+ sh '''
+ if [ "${IS_PRODUCTION:-false}" = "true" ]; then
+ echo "生产部署失败,请查看上方远程部署日志"
+ elif [ "${IS_DEVELOPMENT:-false}" = "true" ]; then
+ docker logs --tail=100 client-web-development || true
+ else
+ docker compose -f docker-compose.test.yml logs --tail=100 || true
+ fi
+ '''
+ }
+ }
+}
diff --git a/README.md b/README.md
index e840607..74d02aa 100644
--- a/README.md
+++ b/README.md
@@ -23,18 +23,28 @@ pnpm build
pnpm test
```
+## 源码组织
+
+业务代码按领域放在 `src/modules//`。每个模块可以包含 `views/`、`components/`、`api/`、`store/`、`types/`、`data/`、`utils/`、`contracts.ts`、`public.ts` 和 `routes.ts`,只创建实际需要的目录。
+
+模块的 `routes.ts` 是路由与导航共用的 manifest。正式页面标记 `release: 'active'`;尚未接入接口、权限或未进入当前范围的占位页面标记 `release: 'prototype'`,不会被注册为可访问路由。`src/modules/registry.ts` 负责统一装配模块,根路由不再逐页维护配置。
+
+跨模块调用只能经过目标模块的 `public.ts`;模块内部 API、store、组件和工具不作为跨模块接口。租户状态、权限映射等稳定模块契约放在 `contracts.ts`。
+
+`src/components/common/` 只存放真实跨模块复用的组件,`src/api/` 只存放 HTTP、Token 会话和统一响应等基础设施。模块私有能力不要提前上移到全局目录。
+
依赖和 `pnpm-lock.yaml` 均保存在当前前端项目中,不需要在仓库根目录安装 Node 依赖。
-一期路由:登录、注册、工作台、我的信息、企业资料、企业成员、角色权限和头程订单。登录、注册、退出、令牌刷新、个人资料、修改密码、企业资料、企业资质、权限资源、角色列表/新增/授权和成员完整维护已经接入一期后端接口;开发代理默认转发到 `http://127.0.0.1:9000`,可通过 `API_PROXY_TARGET` 覆盖。登录使用后端约定的用户名和 8–72 位密码,注册成功后可立即登录,企业资料审核只影响订单提交资格。
+订单与客户路由:登录、注册、工作台、我的信息、企业资料、企业成员、角色权限、头程订单、海外采购、美国本土卡、美本土散板以及海外仓入库订单。跨境下单支持真实列表、详情和新增,海外入库支持当前客户列表与详情;均经海外 Gateway 的客户接口访问,客户范围只取 Token,不提交客户 ID。开发代理通过 `API_PROXY_TARGET` 与 `OVERSEAS_API_PROXY_TARGET` 分别配置国内、海外 Gateway。登录使用后端约定的用户名和 8–72 位密码,注册成功后可立即登录,企业资料审核只影响订单提交资格。
-认证会话保存 Access Token 和轮换后的 Refresh Token,并对并发 401 请求执行单次刷新。客户端只接受 `app_code` 为 `MERCHANT` 的 JWT;页面权限取 JWT 声明与后端启用权限资源的交集。前端权限仅用于导航和交互控制,服务端仍须按用户、租户和权限编码完成最终授权。
+认证会话保存 Access Token 和轮换后的 Refresh Token,并对并发 401 请求执行单次刷新。刷新或会话上下文加载遇到临时网络故障、超时等非认证错误时保留当前有效 Token,只有服务端明确返回 401/会话失效才清理登录状态。客户端只接受 `app_code` 为 `MERCHANT` 的 JWT;页面权限通过当前用户授权接口实时加载,不从 JWT 携带或解析角色权限。前端权限仅用于导航和交互控制,服务端仍须按用户、客户和权限编码完成最终授权。
-本地联调账号提示和默认用户名只在 Vite 开发模式下启用,URL 显式传入的登录用户名仍优先。生产登录表单默认留空,生产构建产物不得包含本地联调账号或开发提示文案。
+登录表单在开发和生产环境默认留空;用户主动选择“记住账号”后只回填登录账号、不保存密码,也不改变 Access/Refresh Token 的持久化和刷新策略;URL 显式传入的登录用户名仍优先。源码和构建产物不得包含联调账号、默认密码或开发提示文案。
-“我的信息”使用真实个人资料和修改密码接口,姓名、电话、邮箱按服务端约束维护;修改密码成功后清理旧会话并返回登录页。成员列表返回姓名、联系方式和 `roleIds`,新增、编辑、角色全量保存、启停和管理员重置密码均使用真实接口,不回退静态成功。角色列表返回 `permissionIds`,页面可回显并全量保存角色权限;角色编辑、启停、删除虽在回归报告中通过,但路径尚未进入当前 Gateway OpenAPI,前端继续保持禁用。
+“我的信息”使用真实个人资料和修改密码接口,姓名、电话、邮箱按服务端约束维护;修改密码成功后清理旧会话并返回登录页。成员列表返回姓名、联系方式和 `roleIds`,新增、编辑、角色全量保存、启停和管理员重置密码均使用真实接口,不回退静态成功。角色页同时加载成员关系,直接打开或刷新后成员数仍按真实 `roleIds` 统计。角色列表返回 `permissionIds`,页面可回显并全量保存角色权限;角色编辑、启停、删除分别调用 `PUT /merchant/v1/iam/roles/{roleId}`、`PUT /merchant/v1/iam/roles/{roleId}/status` 和 `DELETE /merchant/v1/iam/roles/{roleId}`,内置角色及仍有关联成员的角色继续受保护。
-企业资料和资质列表使用真实租户接口,覆盖 `PROFILE_REQUIRED`、`PENDING`、`ACTIVE`、`REJECTED`、`DISABLED` 五种企业状态。企业名称、法定名称和联系人信息支持真实保存;营业执照上传使用文件服务,仅接受 JPG、PNG 且不超过 5MB,文件预览通过当前商户 Token 临时下载并在关闭弹窗后释放。
+企业资料和资质列表使用真实租户接口,覆盖 `PROFILE_REQUIRED`、`PENDING`、`ACTIVE`、`REJECTED`、`DISABLED` 五种企业状态。企业名称、法定名称和联系人信息支持真实保存;营业执照、法人身份证为必填资质,货代资质证书和银行开户许可证为选填资质,均使用文件服务分别上传,接受 PDF、JPG、PNG 且不超过 5MB;两项必填资质齐全后可显式提交审核,文件预览通过当前商户 Token 临时下载并在关闭弹窗后释放。
-头程订单列表、详情、仓库查询、创建/编辑草稿和提交已经接入真实 API,并显式映射后端 `transportMode`、`containerMode` 和订单状态。创建与提交请求携带幂等键,`merchant:order:create`、`merchant:order:edit`、`merchant:order:submit` 分别映射到独立前端操作权限。页面补齐渠道、提货方式、报关模式、目的仓类型、包装类型等后端必填字段;API 模式先清空本地记录,请求失败不会回退模拟成功。收发货单位通过 `/merchant/v1/partners/candidates` 按类型读取启用候选并保存订单快照,不再使用演示单位兜底;快递信息仍由平台维护、客户只读。订单取消不属于本轮本地一期验收,客户端不暴露取消写操作。订单产品状态仍按已确认的七状态执行,不设置“已驳回”;若兼容旧接口数据收到 `REJECTED`,只按待审核展示且不开放编辑。
+头程订单列表、详情、仓库查询、创建/编辑草稿和提交已经接入真实 API,并显式映射后端 `transportMode`、`containerMode` 和订单状态。创建与提交请求携带幂等键,`merchant:order:create`、`merchant:order:edit`、`merchant:order:submit` 分别映射到独立前端操作权限。页面补齐渠道、提货方式、报关模式、目的仓类型、包装类型等后端必填字段;API 模式先清空本地记录,请求失败不会回退模拟成功。FBA 和海外仓按公共仓类型筛选,选择后由仓库主数据生成目的仓字段,不再要求客户重复选择收货单位;私人仓才填写并按当前用户缓存收件资料。只有“我司提货”需要填写提货联系人和地址,其他送货方式不发送发货方快照,订单也不再依赖往来单位 `customerId`。快递信息仍由平台维护、客户只读。订单取消不属于本轮本地订单与客户验收,客户端不暴露取消写操作。订单产品状态仍按已确认的七状态执行,不设置“已驳回”;若兼容旧接口数据收到 `REJECTED`,只按待审核展示且不开放编辑。
-联调远端 Gateway 时可在启动命令前设置 `API_PROXY_TARGET=http://192.168.110.149:9000`;进程环境变量优先于 `.env` 文件。浏览器仍统一请求同源 `/api`,源码中不写死业务服务地址。
+联调远端 Gateway 时,进程环境变量优先于 `.env` 文件。浏览器国内业务统一请求同源 `/api`,海外业务请求同源 `/overseas-api`,由代理分别转发并确保最终路径只有一个 `/api` 前缀。联调时可只读检查同工作区的 `baiyue-firstleg/` Controller、DTO、Service、Repository 和回归脚本,用于发现后端拉取后的契约变化,但不修改后端仓库。
diff --git a/docker-compose.development.yml b/docker-compose.development.yml
new file mode 100644
index 0000000..d52a739
--- /dev/null
+++ b/docker-compose.development.yml
@@ -0,0 +1,21 @@
+services:
+ client-web:
+ image: client-web-development:${IMAGE_TAG:?IMAGE_TAG is required}
+ container_name: client-web-development
+ restart: unless-stopped
+ ports:
+ - "127.0.0.1:9002:80"
+ environment:
+ API_UPSTREAM: "http://firstleg-development-gateway:9000"
+ OVERSEAS_API_UPSTREAM: "http://firstleg-development-gateway-overseas:9300"
+ networks:
+ - development-backend
+ - development-overseas-backend
+
+networks:
+ development-backend:
+ external: true
+ name: firstleg-development-network
+ development-overseas-backend:
+ external: true
+ name: firstleg-development-overseas-network
diff --git a/docker-compose.production.yml b/docker-compose.production.yml
new file mode 100644
index 0000000..167b779
--- /dev/null
+++ b/docker-compose.production.yml
@@ -0,0 +1,19 @@
+name: firstleg-prod-client-web
+
+services:
+ client-web:
+ image: ${IMAGE_REPOSITORY:?IMAGE_REPOSITORY is required}:${IMAGE_TAG:?IMAGE_TAG is required}
+ container_name: firstleg-prod-client-web
+ restart: unless-stopped
+ ports:
+ - "127.0.0.1:${HOST_PORT:-9002}:80"
+ environment:
+ API_UPSTREAM: "http://gateway:9000"
+ OVERSEAS_API_UPSTREAM: "http://gateway-overseas:9300"
+ networks:
+ - infra-network
+
+networks:
+ infra-network:
+ external: true
+ name: firstleg-prod-infra-network
diff --git a/docker-compose.test.yml b/docker-compose.test.yml
new file mode 100644
index 0000000..c3d40ea
--- /dev/null
+++ b/docker-compose.test.yml
@@ -0,0 +1,17 @@
+services:
+ client-web:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ args:
+ VITE_ADMIN_WEB_ORIGIN: "${VITE_ADMIN_WEB_ORIGIN:-https://test-admin.buggoglobal.com}"
+ NODE_IMAGE: "${NODE_BASE_IMAGE:-node:22-alpine}"
+ NGINX_IMAGE: "${NGINX_BASE_IMAGE:-nginx:1.27-alpine}"
+ image: client-web-test:local
+ container_name: client-web-test
+ restart: unless-stopped
+ ports:
+ - "${HOST_PORT:-9002}:80"
+ environment:
+ API_UPSTREAM: "http://test-api-cn.buggoglobal.com"
+ OVERSEAS_API_UPSTREAM: "http://test-api-global.buggoglobal.com"
diff --git a/index.html b/index.html
index be4f977..33f7886 100644
--- a/index.html
+++ b/index.html
@@ -3,8 +3,9 @@
-
- 虫虫出海客户工作台
+
+
+ 柏悦物流客户工作台
@@ -12,3 +13,4 @@