Procházet zdrojové kódy

feat: industry invoice platform - full module

alphaH před 1 měsícem
rodič
revize
c333e566e5
90 změnil soubory, kde provedl 4820 přidání a 1 odebrání
  1. 16 0
      frontend/src/api/module_payment/invoice/account.ts
  2. 24 0
      frontend/src/api/module_payment/invoice/company.ts
  3. 29 0
      frontend/src/api/module_payment/invoice/employee.ts
  4. 31 0
      frontend/src/api/module_payment/invoice/goods.ts
  5. 119 0
      frontend/src/api/module_payment/invoice/order.ts
  6. 46 0
      frontend/src/api/module_payment/invoice/supplier.ts
  7. 31 0
      frontend/src/api/module_payment/invoice/task.ts
  8. 16 0
      frontend/src/api/module_payment/invoice/tax.ts
  9. 113 0
      frontend/src/mock/module_payment/invoice/index.ts
  10. 72 0
      frontend/src/views/module_payment/invoice/account/index.vue
  11. 144 0
      frontend/src/views/module_payment/invoice/company/index.vue
  12. 75 0
      frontend/src/views/module_payment/invoice/employee/components/EmployeeFormDialog.vue
  13. 72 0
      frontend/src/views/module_payment/invoice/employee/index.vue
  14. 90 0
      frontend/src/views/module_payment/invoice/goods/index.vue
  15. 59 0
      frontend/src/views/module_payment/invoice/order/components/BatchImportDialog.vue
  16. 81 0
      frontend/src/views/module_payment/invoice/order/components/OrderDetailDialog.vue
  17. 44 0
      frontend/src/views/module_payment/invoice/order/components/OrderInvoiceDialog.vue
  18. 156 0
      frontend/src/views/module_payment/invoice/order/index.vue
  19. 79 0
      frontend/src/views/module_payment/invoice/supplier/components/SupplierFormDialog.vue
  20. 86 0
      frontend/src/views/module_payment/invoice/supplier/index.vue
  21. 71 0
      frontend/src/views/module_payment/invoice/task/index.vue
  22. 52 0
      frontend/src/views/module_payment/invoice/tax/index.vue
  23. 305 0
      java/.claude/plan/code-review-invoice-platform.md
  24. 682 0
      java/.claude/plan/industry-invoice-platform.md
  25. 237 0
      java/sql/012_invoice_tables.sql
  26. 48 0
      java/sql/013_invoice_menu.sql
  27. 3 1
      java/src/main/java/com/payment/platform/core/tenant/TenantInnerInterceptor.java
  28. 34 0
      java/src/main/java/com/payment/platform/module/payment/invoice/account/controller/AccountController.java
  29. 23 0
      java/src/main/java/com/payment/platform/module/payment/invoice/account/dto/AccountVO.java
  30. 23 0
      java/src/main/java/com/payment/platform/module/payment/invoice/account/entity/TransferAccountEntity.java
  31. 9 0
      java/src/main/java/com/payment/platform/module/payment/invoice/account/mapper/TransferAccountMapper.java
  32. 32 0
      java/src/main/java/com/payment/platform/module/payment/invoice/account/service/TransferAccountService.java
  33. 40 0
      java/src/main/java/com/payment/platform/module/payment/invoice/company/controller/CompanyController.java
  34. 11 0
      java/src/main/java/com/payment/platform/module/payment/invoice/company/dto/CompanyConfigUpdateDTO.java
  35. 36 0
      java/src/main/java/com/payment/platform/module/payment/invoice/company/dto/CompanyConfigVO.java
  36. 37 0
      java/src/main/java/com/payment/platform/module/payment/invoice/company/entity/CompanyConfigEntity.java
  37. 9 0
      java/src/main/java/com/payment/platform/module/payment/invoice/company/mapper/CompanyConfigMapper.java
  38. 105 0
      java/src/main/java/com/payment/platform/module/payment/invoice/company/service/CompanyConfigService.java
  39. 50 0
      java/src/main/java/com/payment/platform/module/payment/invoice/employee/controller/EmployeeController.java
  40. 19 0
      java/src/main/java/com/payment/platform/module/payment/invoice/employee/dto/EmployeeCreateDTO.java
  41. 12 0
      java/src/main/java/com/payment/platform/module/payment/invoice/employee/dto/EmployeeQueryDTO.java
  42. 21 0
      java/src/main/java/com/payment/platform/module/payment/invoice/employee/dto/EmployeeVO.java
  43. 19 0
      java/src/main/java/com/payment/platform/module/payment/invoice/employee/entity/EmployeeEntity.java
  44. 16 0
      java/src/main/java/com/payment/platform/module/payment/invoice/employee/enums/EmployeeEnums.java
  45. 9 0
      java/src/main/java/com/payment/platform/module/payment/invoice/employee/mapper/EmployeeMapper.java
  46. 120 0
      java/src/main/java/com/payment/platform/module/payment/invoice/employee/service/EmployeeService.java
  47. 57 0
      java/src/main/java/com/payment/platform/module/payment/invoice/goods/controller/GoodsController.java
  48. 17 0
      java/src/main/java/com/payment/platform/module/payment/invoice/goods/dto/GoodsCategoryVO.java
  49. 18 0
      java/src/main/java/com/payment/platform/module/payment/invoice/goods/dto/GoodsCreateDTO.java
  50. 11 0
      java/src/main/java/com/payment/platform/module/payment/invoice/goods/dto/GoodsQueryDTO.java
  51. 20 0
      java/src/main/java/com/payment/platform/module/payment/invoice/goods/dto/GoodsVO.java
  52. 17 0
      java/src/main/java/com/payment/platform/module/payment/invoice/goods/entity/GoodsCategoryEntity.java
  53. 18 0
      java/src/main/java/com/payment/platform/module/payment/invoice/goods/entity/GoodsEntity.java
  54. 9 0
      java/src/main/java/com/payment/platform/module/payment/invoice/goods/mapper/GoodsCategoryMapper.java
  55. 9 0
      java/src/main/java/com/payment/platform/module/payment/invoice/goods/mapper/GoodsMapper.java
  56. 165 0
      java/src/main/java/com/payment/platform/module/payment/invoice/goods/service/GoodsService.java
  57. 64 0
      java/src/main/java/com/payment/platform/module/payment/invoice/order/controller/OrderController.java
  58. 11 0
      java/src/main/java/com/payment/platform/module/payment/invoice/order/dto/InvoiceVO.java
  59. 6 0
      java/src/main/java/com/payment/platform/module/payment/invoice/order/dto/OrderBatchImportDTO.java
  60. 19 0
      java/src/main/java/com/payment/platform/module/payment/invoice/order/dto/OrderCreateDTO.java
  61. 16 0
      java/src/main/java/com/payment/platform/module/payment/invoice/order/dto/OrderItemVO.java
  62. 21 0
      java/src/main/java/com/payment/platform/module/payment/invoice/order/dto/OrderQueryDTO.java
  63. 42 0
      java/src/main/java/com/payment/platform/module/payment/invoice/order/dto/OrderVO.java
  64. 12 0
      java/src/main/java/com/payment/platform/module/payment/invoice/order/dto/TaxDetailVO.java
  65. 41 0
      java/src/main/java/com/payment/platform/module/payment/invoice/order/entity/OrderEntity.java
  66. 21 0
      java/src/main/java/com/payment/platform/module/payment/invoice/order/entity/OrderItemEntity.java
  67. 19 0
      java/src/main/java/com/payment/platform/module/payment/invoice/order/entity/TaxDetailEntity.java
  68. 30 0
      java/src/main/java/com/payment/platform/module/payment/invoice/order/enums/OrderEnums.java
  69. 9 0
      java/src/main/java/com/payment/platform/module/payment/invoice/order/mapper/OrderItemMapper.java
  70. 9 0
      java/src/main/java/com/payment/platform/module/payment/invoice/order/mapper/OrderMapper.java
  71. 9 0
      java/src/main/java/com/payment/platform/module/payment/invoice/order/mapper/TaxDetailMapper.java
  72. 219 0
      java/src/main/java/com/payment/platform/module/payment/invoice/order/service/OrderService.java
  73. 55 0
      java/src/main/java/com/payment/platform/module/payment/invoice/supplier/controller/SupplierController.java
  74. 19 0
      java/src/main/java/com/payment/platform/module/payment/invoice/supplier/dto/SupplierCreateDTO.java
  75. 14 0
      java/src/main/java/com/payment/platform/module/payment/invoice/supplier/dto/SupplierQueryDTO.java
  76. 20 0
      java/src/main/java/com/payment/platform/module/payment/invoice/supplier/dto/SupplierVO.java
  77. 18 0
      java/src/main/java/com/payment/platform/module/payment/invoice/supplier/entity/SupplierEntity.java
  78. 24 0
      java/src/main/java/com/payment/platform/module/payment/invoice/supplier/enums/SupplierEnums.java
  79. 9 0
      java/src/main/java/com/payment/platform/module/payment/invoice/supplier/mapper/SupplierMapper.java
  80. 122 0
      java/src/main/java/com/payment/platform/module/payment/invoice/supplier/service/SupplierService.java
  81. 31 0
      java/src/main/java/com/payment/platform/module/payment/invoice/task/controller/TaskController.java
  82. 12 0
      java/src/main/java/com/payment/platform/module/payment/invoice/task/dto/TaskQueryDTO.java
  83. 24 0
      java/src/main/java/com/payment/platform/module/payment/invoice/task/dto/TaskVO.java
  84. 25 0
      java/src/main/java/com/payment/platform/module/payment/invoice/task/entity/TaskEntity.java
  85. 30 0
      java/src/main/java/com/payment/platform/module/payment/invoice/task/enums/TaskEnums.java
  86. 9 0
      java/src/main/java/com/payment/platform/module/payment/invoice/task/mapper/TaskMapper.java
  87. 30 0
      java/src/main/java/com/payment/platform/module/payment/invoice/task/service/TaskService.java
  88. 36 0
      java/src/main/java/com/payment/platform/module/payment/invoice/tax/controller/TaxController.java
  89. 13 0
      java/src/main/java/com/payment/platform/module/payment/invoice/tax/dto/TaxConfigVO.java
  90. 34 0
      java/src/main/java/com/payment/platform/module/payment/invoice/tax/service/TaxService.java

+ 16 - 0
frontend/src/api/module_payment/invoice/account.ts

@@ -0,0 +1,16 @@
+// 行业发票平台 — 转账账户 API
+import request from "@/utils/request";
+
+export interface AccountVO {
+  id?: number; total_amount?: number; pending_amount?: number;
+  bank_account_name?: string; bank_account_no?: string; bank_name?: string;
+  bank_branch?: string; bank_location?: string; bank_code?: string;
+  status?: string; created_time?: string; updated_time?: string;
+}
+
+export function getAccountInfo() {
+  return request.get<AccountVO>("/payment/invoice/account");
+}
+export function refreshAccount() {
+  return request.put<AccountVO>("/payment/invoice/account/refresh");
+}

+ 24 - 0
frontend/src/api/module_payment/invoice/company.ts

@@ -0,0 +1,24 @@
+// 行业发票平台 — 企业信息 API
+import request from "@/utils/request";
+
+export interface CompanyConfigVO {
+  id?: number; company_name?: string; tax_no?: string; bank_name?: string;
+  bank_account?: string; address?: string; phone?: string; tax_region?: string;
+  issuer_name?: string; issuer_id_card?: string;
+  contact_name?: string; contact_phone?: string;
+  monthly_quota?: number; available_quota?: number; downloaded_quota?: number; used_quota?: number;
+  tax_method?: string; default_invoice_type?: string; default_tax_rate?: string;
+  require_audit_before_pay?: boolean; invite_to_supplier?: boolean;
+  show_payee_name?: boolean; employee_quota_enabled?: boolean;
+  status?: string; created_time?: string; updated_time?: string;
+}
+
+export function getCompanyConfig() {
+  return request.get<CompanyConfigVO>("/payment/invoice/company");
+}
+export function updateCompanyInfo(data: Record<string, any>) {
+  return request.put<CompanyConfigVO>("/payment/invoice/company/info", data);
+}
+export function updateInvoiceInfo(data: Record<string, any>) {
+  return request.put<CompanyConfigVO>("/payment/invoice/company/invoice", data);
+}

+ 29 - 0
frontend/src/api/module_payment/invoice/employee.ts

@@ -0,0 +1,29 @@
+// 行业发票平台 — 员工 API
+import request from "@/utils/request";
+
+export interface EmployeeVO {
+  id?: number; name?: string; phone?: string; id_card?: string;
+  role?: string; allow_select_supplier?: boolean; sys_user_id?: number;
+  status?: string; created_time?: string; updated_time?: string;
+}
+
+export interface PageResult<T> { list: T[]; total: number; page_no: number; page_size: number; has_next: boolean; }
+
+export const ROLE_OPTIONS = [
+  { label: "超级管理员", value: "SUPER_ADMIN" },
+  { label: "营业员", value: "CLERK" },
+  { label: "开票员", value: "ISSUER" },
+];
+
+export function getEmployeeList(params: Record<string, any>) {
+  return request.get<PageResult<EmployeeVO>>("/payment/invoice/employee", { params });
+}
+export function createEmployee(data: Record<string, any>) {
+  return request.post<EmployeeVO>("/payment/invoice/employee", data);
+}
+export function updateEmployee(id: number, data: Record<string, any>) {
+  return request.put<EmployeeVO>(`/payment/invoice/employee/${id}`, data);
+}
+export function deleteEmployee(id: number) {
+  return request.delete(`/payment/invoice/employee/${id}`);
+}

+ 31 - 0
frontend/src/api/module_payment/invoice/goods.ts

@@ -0,0 +1,31 @@
+// 行业发票平台 — 商品 API
+import request from "@/utils/request";
+
+export interface GoodsCategoryVO {
+  id: number; parent_id: number; name: string;
+  sort_order?: number; product_code?: string; children: GoodsCategoryVO[];
+}
+
+export interface GoodsVO {
+  id?: number; category_id?: number; name?: string; unit?: string;
+  spec?: string; enterprise_id?: string; status?: string;
+  created_time?: string; updated_time?: string;
+}
+
+export interface PageResult<T> { list: T[]; total: number; page_no: number; page_size: number; has_next: boolean; }
+
+export function getCategoryTree() {
+  return request.get<GoodsCategoryVO[]>("/payment/invoice/goods/category");
+}
+export function getGoodsList(params: Record<string, any>) {
+  return request.get<PageResult<GoodsVO>>("/payment/invoice/goods", { params });
+}
+export function createGoods(data: Record<string, any>) {
+  return request.post<GoodsVO>("/payment/invoice/goods", data);
+}
+export function updateGoods(id: number, data: Record<string, any>) {
+  return request.put<GoodsVO>(`/payment/invoice/goods/${id}`, data);
+}
+export function deleteGoods(id: number) {
+  return request.delete(`/payment/invoice/goods/${id}`);
+}

+ 119 - 0
frontend/src/api/module_payment/invoice/order.ts

@@ -0,0 +1,119 @@
+// 行业发票平台 — 订单 API + 类型定义
+import request from "@/utils/request";
+
+// ==================== 类型定义 ====================
+
+export interface OrderItemVO {
+  id?: number;
+  order_id?: number;
+  seq_no?: number;
+  goods_name?: string;
+  unit_price?: number;
+  quantity?: number;
+  amount?: number | null;
+}
+
+export interface TaxDetailVO {
+  id?: number;
+  order_id?: number;
+  tax_type?: string;
+  tax_name?: string;
+  tax_amount?: number;
+}
+
+export interface OrderVO {
+  id?: number;
+  order_no?: string;
+  alipay_trade_no?: string;
+  order_time?: string;
+  payment_time?: string;
+  natural_person_name?: string;
+  natural_person_phone?: string;
+  collection_account_type?: string;
+  collection_account?: string;
+  tax_amount?: number;
+  trade_status?: string;
+  invoice_no?: string;
+  invoice_pre_tax_amount?: number;
+  invoice_tax_amount?: number;
+  red_invoice_no?: string;
+  clerk_name?: string;
+  attachments?: string | null;
+  product_code?: string;
+  order_total_amount?: number;
+  goods_amount?: number;
+  total_tax_paid?: number;
+  personal_income_tax?: number;
+  value_added_tax?: number;
+  urban_maintenance_tax?: number;
+  education_surcharge?: number;
+  local_education_surcharge?: number;
+  status?: string;
+  created_time?: string;
+  updated_time?: string;
+  items?: OrderItemVO[];
+  tax_details?: TaxDetailVO[];
+}
+
+export interface InvoiceVO {
+  type: string; // RED / BLUE
+  invoice_no?: string;
+  tax_amount?: number;
+  red_status?: string;
+}
+
+export interface PageResult<T> {
+  list: T[];
+  total: number;
+  page_no: number;
+  page_size: number;
+  has_next: boolean;
+}
+
+// ==================== 枚举常量 ====================
+
+export const TRADE_STATUS_OPTIONS = [
+  { label: "待关联", value: "WAIT_LINK" },
+  { label: "待审核", value: "WAIT_AUDIT" },
+  { label: "待确认", value: "WAIT_CONFIRM" },
+  { label: "已确认", value: "CONFIRMED" },
+  { label: "待支付", value: "WAIT_PAY" },
+  { label: "交易成功", value: "SUCCESS" },
+  { label: "订单取消", value: "CANCELLED" },
+  { label: "订单失败", value: "FAILED" },
+];
+
+export const ACCOUNT_TYPE_OPTIONS = [
+  { label: "支付宝", value: "ALIPAY" },
+  { label: "银行卡", value: "BANKCARD" },
+];
+
+// ==================== API 函数 ====================
+
+export function getOrderList(params: Record<string, any>) {
+  return request.get<PageResult<OrderVO>>("/payment/invoice/order", { params });
+}
+
+export function getOrderDetail(id: number) {
+  return request.get<OrderVO>(`/payment/invoice/order/${id}`);
+}
+
+export function getOrderInvoice(id: number) {
+  return request.get<InvoiceVO[]>(`/payment/invoice/order/${id}/invoice`);
+}
+
+export function batchImportOrder(data: { file_url: string }) {
+  return request.post("/payment/invoice/order/batch-import", data);
+}
+
+export function batchCancelOrders(ids: number[]) {
+  return request.post("/payment/invoice/order/batch-cancel", ids);
+}
+
+export function exportOrders() {
+  return request.get("/payment/invoice/order/export/order");
+}
+
+export function exportInvoices() {
+  return request.get("/payment/invoice/order/export/invoice");
+}

+ 46 - 0
frontend/src/api/module_payment/invoice/supplier.ts

@@ -0,0 +1,46 @@
+// 行业发票平台 — 供应商 API
+import request from "@/utils/request";
+
+export interface SupplierVO {
+  id?: number;
+  name?: string;
+  account_type?: string;
+  account_no?: string;
+  phone?: string;
+  confirm_status?: string;
+  status?: string;
+  created_time?: string;
+  updated_time?: string;
+}
+
+export interface PageResult<T> { list: T[]; total: number; page_no: number; page_size: number; has_next: boolean; }
+
+export const SUPPLIER_ACCOUNT_TYPE_OPTIONS = [
+  { label: "支付宝手机号", value: "PHONE" },
+  { label: "支付宝邮箱", value: "EMAIL" },
+];
+
+export const CONFIRM_STATUS_OPTIONS = [
+  { label: "待确认", value: "PENDING" },
+  { label: "已确认", value: "CONFIRMED" },
+];
+
+export function getSupplierList(params: Record<string, any>) {
+  return request.get<PageResult<SupplierVO>>("/payment/invoice/supplier", { params });
+}
+
+export function createSupplier(data: Record<string, any>) {
+  return request.post<SupplierVO>("/payment/invoice/supplier", data);
+}
+
+export function updateSupplier(id: number, data: Record<string, any>) {
+  return request.put<SupplierVO>(`/payment/invoice/supplier/${id}`, data);
+}
+
+export function deleteSupplier(id: number) {
+  return request.delete(`/payment/invoice/supplier/${id}`);
+}
+
+export function batchImportSuppliers(data: Record<string, any>) {
+  return request.post("/payment/invoice/supplier/batch-import", data);
+}

+ 31 - 0
frontend/src/api/module_payment/invoice/task.ts

@@ -0,0 +1,31 @@
+// 行业发票平台 — 任务中心 API
+import request from "@/utils/request";
+
+export interface TaskVO {
+  id?: number; start_time?: string; finish_time?: string;
+  product?: string; task_type?: string; task_status?: string;
+  file_url?: string; total_count?: number; success_count?: number; fail_count?: number;
+  error_msg?: string; status?: string; created_time?: string; updated_time?: string;
+}
+
+export interface PageResult<T> { list: T[]; total: number; page_no: number; page_size: number; has_next: boolean; }
+
+export const TASK_TYPE_OPTIONS = [
+  { label: "订单导入", value: "ORDER_IMPORT" },
+  { label: "供应商导入", value: "SUPPLIER_IMPORT" },
+  { label: "交易及发票导出", value: "TRADE_INVOICE_EXPORT" },
+  { label: "交易导出", value: "TRADE_EXPORT" },
+  { label: "发票导出", value: "INVOICE_EXPORT" },
+  { label: "佐证材料导出", value: "MATERIAL_EXPORT" },
+  { label: "缴税记录导出", value: "TAX_RECORD_EXPORT" },
+];
+
+export const TASK_STATUS_OPTIONS = [
+  { label: "进行中", value: "RUNNING" },
+  { label: "已完成", value: "COMPLETED" },
+  { label: "失败", value: "FAILED" },
+];
+
+export function getTaskList(params: Record<string, any>) {
+  return request.get<PageResult<TaskVO>>("/payment/invoice/task", { params });
+}

+ 16 - 0
frontend/src/api/module_payment/invoice/tax.ts

@@ -0,0 +1,16 @@
+// 行业发票平台 — 缴税管理 API
+import request from "@/utils/request";
+
+export interface TaxConfigVO {
+  taxMode: string; // PERSONAL / ENTERPRISE
+  taxModeLabel: string;
+  enterpriseEnabled: boolean;
+  description: string;
+}
+
+export function getTaxConfig() {
+  return request.get<TaxConfigVO>("/payment/invoice/tax/config");
+}
+export function updateTaxConfig(data: { taxMode: string }) {
+  return request.put<TaxConfigVO>("/payment/invoice/tax/config", data);
+}

+ 113 - 0
frontend/src/mock/module_payment/invoice/index.ts

@@ -0,0 +1,113 @@
+// 行业发票平台 — Mock 数据
+import type { OrderVO, PageResult, InvoiceVO } from "@/api/module_payment/invoice/order";
+import type { SupplierVO } from "@/api/module_payment/invoice/supplier";
+import type { EmployeeVO } from "@/api/module_payment/invoice/employee";
+import type { GoodsCategoryVO, GoodsVO } from "@/api/module_payment/invoice/goods";
+import type { CompanyConfigVO } from "@/api/module_payment/invoice/company";
+import type { TaskVO } from "@/api/module_payment/invoice/task";
+import type { AccountVO } from "@/api/module_payment/invoice/account";
+import type { TaxConfigVO } from "@/api/module_payment/invoice/tax";
+
+export function mockOrderList(params: Record<string, any>): PageResult<OrderVO> {
+  const items: OrderVO[] = [{
+    id: 1001, order_no: "2026070700152005720055879271",
+    alipay_trade_no: "20260707020070061550010074092577",
+    order_time: "2026-07-07T15:08:51+08:00", payment_time: "2026-07-07T15:12:23+08:00",
+    natural_person_name: "刘祥权", natural_person_phone: "18684748729",
+    collection_account_type: "ALIPAY", tax_amount: 10.0, trade_status: "SUCCESS",
+    invoice_no: "26437200000005616855", invoice_pre_tax_amount: 9.9, invoice_tax_amount: 0.1,
+    red_invoice_no: "26437200000005616856", clerk_name: "姚双", product_code: "SCRAP_MATERIAL",
+    order_total_amount: 10.0, goods_amount: 9.98, total_tax_paid: 0.02,
+    personal_income_tax: 0.02, value_added_tax: 0.0, urban_maintenance_tax: 0.0,
+    education_surcharge: 0.0, local_education_surcharge: 0.0,
+    status: "1", created_time: "2026-07-07T15:08:51+08:00",
+    updated_time: "2026-07-07T15:12:23+08:00",
+  }];
+  return { items, total: 1, page_no: params.pageNo || 1, page_size: params.pageSize || 10 };
+}
+
+export function mockOrderDetail(id: number): OrderVO {
+  return {
+    ...mockOrderList({}).items[0], id,
+    items: [{ id: 2001, order_id: id, seq_no: 1, goods_name: "废旧电线电缆拆解物", unit_price: 100.0, quantity: 0.1, amount: null }],
+    tax_details: [
+      { id: 3001, order_id: id, tax_type: "GOODS", tax_name: "货款金额", tax_amount: 9.98 },
+      { id: 3002, order_id: id, tax_type: "PIT", tax_name: "个人所得税", tax_amount: 0.02 },
+      { id: 3003, order_id: id, tax_type: "VAT", tax_name: "增值税", tax_amount: 0.0 },
+    ],
+  };
+}
+
+export function mockOrderInvoice(): InvoiceVO[] {
+  return [
+    { type: "RED", invoice_no: "26437200000005616856", tax_amount: 10.0, red_status: "RED_SUCCESS" },
+    { type: "BLUE", invoice_no: "26437200000005616855", tax_amount: 10.0 },
+  ];
+}
+
+export function mockSupplierList(params: Record<string, any>): PageResult<SupplierVO> {
+  const items: SupplierVO[] = [{ id: 1, name: "刘祥权", account_type: "PHONE", account_no: "18684748729", phone: "18684748729", confirm_status: "CONFIRMED", status: "1", created_time: "2026-01-01T00:00:00+08:00", updated_time: "2026-01-01T00:00:00+08:00" }];
+  return { items, total: 1, page_no: params.pageNo || 1, page_size: params.pageSize || 10 };
+}
+
+export function mockEmployeeList(params: Record<string, any>): PageResult<EmployeeVO> {
+  const items: EmployeeVO[] = [
+    { id: 1, name: "姚双", phone: "1****************0", role: "CLERK", allow_select_supplier: true, status: "1", created_time: "2026-01-01T00:00:00+08:00", updated_time: "2026-01-01T00:00:00+08:00" },
+    { id: 2, name: "湖南省铭恩商务管理有限公司", role: "SUPER_ADMIN", allow_select_supplier: false, status: "1", created_time: "2026-01-01T00:00:00+08:00", updated_time: "2026-01-01T00:00:00+08:00" },
+    { id: 3, name: "童述", id_card: "4****************4", role: "ISSUER", allow_select_supplier: false, status: "1", created_time: "2026-01-01T00:00:00+08:00", updated_time: "2026-01-01T00:00:00+08:00" },
+  ];
+  return { items, total: 3, page_no: params.pageNo || 1, page_size: params.pageSize || 10 };
+}
+
+export function mockCategoryTree(): GoodsCategoryVO[] {
+  const c = (id: number, pid: number, n: string, o: number, ch: GoodsCategoryVO[] = []): GoodsCategoryVO => ({ id, parent_id: pid, name: n, sort_order: o, children: ch });
+  return [c(1, 0, "报废产品", 0, [
+    c(2, 1, "废钢铁", 1, [c(21, 2, "制造性废钢铁", 1), c(22, 2, "农业废钢铁", 2), c(23, 2, "建筑业废钢铁", 3), c(24, 2, "家用废钢铁", 4), c(25, 2, "机器设备废钢铁", 5), c(26, 2, "其他废钢铁", 6)]),
+    c(3, 1, "废有色金属", 2, [c(31, 3, "废铜", 1), c(32, 3, "废铝", 2), c(33, 3, "废铅", 3), c(34, 3, "废锌", 4), c(35, 3, "废稀贵金属", 5), c(36, 3, "其他废有色金属", 6)]),
+    c(4, 1, "废塑料", 3), c(5, 1, "废轮胎", 4), c(6, 1, "废纸", 5),
+    c(7, 1, "废弃电器电子产品", 6, [c(71, 7, "废电视机", 1), c(72, 7, "废电冰箱", 2), c(73, 7, "废洗衣机", 3), c(74, 7, "废空调", 4), c(75, 7, "废电脑", 5), c(76, 7, "废手机", 6), c(77, 7, "其他废弃电器电子产品", 7)]),
+    c(8, 1, "报废机动车", 7, [c(81, 8, "报废汽车", 1), c(82, 8, "报废摩托车", 2), c(83, 8, "其他报废机动车", 3)]),
+    c(9, 1, "废旧纺织品", 8), c(10, 1, "废玻璃", 9),
+    c(11, 1, "废电池", 10, [c(111, 11, "废铅蓄电池", 1), c(112, 11, "废锂离子电池", 2), c(113, 11, "废镍氢电池", 3), c(114, 11, "其他废电池", 4)]),
+    c(12, 1, "其他报废产品", 11, [c(121, 12, "其他生活类报废产品", 1), c(122, 12, "报废船舶", 2), c(123, 12, "其他未列明报废产品", 3)]),
+  ])];
+}
+
+export function mockGoodsList(params: Record<string, any>): PageResult<GoodsVO> {
+  return { items: [], total: 0, page_no: params.pageNo || 1, page_size: params.pageSize || 10 };
+}
+
+export function mockCompanyConfig(): CompanyConfigVO {
+  return {
+    id: 1, company_name: "湖南省铭恩商务管理有限公司", tax_no: "91430681MACKY5ABXW",
+    bank_name: "中国光大银行股份有限公司岳阳汨罗支行", bank_account: "53390188000096012",
+    address: "湖南省岳阳市汨罗市新市镇循环经济产业园鸿昱新路南侧天立路西侧(办公楼)101-201室",
+    phone: "-", tax_region: "国家税务总局汨罗市税务局第二税务所", issuer_name: "童述", issuer_id_card: "4****************4",
+    contact_name: "姚双", contact_phone: "18810729710",
+    monthly_quota: 10000000.0, available_quota: 9999990.0, downloaded_quota: 10.0, used_quota: 0.0,
+    tax_method: "SIMPLIFIED", default_invoice_type: "ORDINARY", default_tax_rate: "1%",
+    require_audit_before_pay: false, invite_to_supplier: false, show_payee_name: false, employee_quota_enabled: false,
+    status: "1", created_time: "2026-01-01T00:00:00+08:00", updated_time: "2026-01-01T00:00:00+08:00",
+  };
+}
+
+export function mockTaskList(params: Record<string, any>): PageResult<TaskVO> {
+  return { items: [], total: 0, page_no: params.pageNo || 1, page_size: params.pageSize || 10 };
+}
+
+export function mockAccountInfo(): AccountVO {
+  return {
+    id: 1, total_amount: 40.0, pending_amount: 0.0,
+    bank_account_name: "支付宝支付科技有限公司", bank_account_no: "2088882400215288826",
+    bank_name: "支付机构备付金集中存管账户", bank_branch: "支付宝-备付金账户",
+    bank_location: "上海市-上海市", bank_code: "991290000015",
+    status: "1", created_time: "2026-01-01T00:00:00+08:00", updated_time: "2026-01-01T00:00:00+08:00",
+  };
+}
+
+export function mockTaxConfig(): TaxConfigVO {
+  return {
+    tax_mode: "PERSONAL", tax_mode_label: "个人缴税", enterprise_enabled: false,
+    description: "选择个人缴纳时,由自然人在收款时自行缴纳订单产生的增值税/附加税/个税等税费。",
+  };
+}

+ 72 - 0
frontend/src/views/module_payment/invoice/account/index.vue

@@ -0,0 +1,72 @@
+<template>
+  <div class="app-container">
+    <h3>转账账户管理</h3>
+    <el-row :gutter="16" style="margin-top:16px">
+      <el-col :span="8">
+        <el-card shadow="never">
+          <template #header><span style="font-weight:600">转账账户资金</span></template>
+          <div style="text-align:center;padding:16px 0">
+            <p style="color:#909399;font-size:14px">账户总金额 (元)</p>
+            <p style="font-size:32px;font-weight:700;color:#303133;margin:8px 0">{{ account.total_amount?.toFixed(2) }}</p>
+            <el-button size="small" @click="refresh">刷新</el-button>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card shadow="never">
+          <template #header><span style="font-weight:600">待支付金额 (元)</span></template>
+          <div style="text-align:center;padding:16px 0">
+            <p style="font-size:32px;font-weight:700;color:#303133;margin:8px 0">{{ account.pending_amount?.toFixed(2) }}</p>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-card shadow="never" style="margin-top:16px">
+      <template #header>
+        <div style="display:flex;justify-content:space-between;align-items:center">
+          <span style="font-weight:600">如何充值</span>
+          <el-button text @click="rechargeExpanded = !rechargeExpanded">{{ rechargeExpanded ? '收起' : '展开' }}</el-button>
+        </div>
+      </template>
+      <div v-if="rechargeExpanded">
+        <el-steps :space="200" finish-status="success" style="margin-bottom:20px">
+          <el-step title="1" description="登录你的网上银行" />
+          <el-step title="2" description="转账时填写以下专属账户" />
+          <el-step title="3" description="转账完成后自动充值到账" />
+        </el-steps>
+        <el-descriptions :column="1" border size="small">
+          <el-descriptions-item label="户 名">{{ account.bank_account_name }} <el-button link size="small">复制</el-button></el-descriptions-item>
+          <el-descriptions-item label="账 号">{{ account.bank_account_no }} <el-button link size="small">复制</el-button></el-descriptions-item>
+          <el-descriptions-item label="银 行">{{ account.bank_name }}</el-descriptions-item>
+          <el-descriptions-item label="支 行">{{ account.bank_branch }}</el-descriptions-item>
+          <el-descriptions-item label="开户地">{{ account.bank_location }}</el-descriptions-item>
+          <el-descriptions-item label="开户行号">{{ account.bank_code }}</el-descriptions-item>
+        </el-descriptions>
+      </div>
+    </el-card>
+
+    <el-alert type="info" :closable="false" style="margin-top:16px" show-icon>
+      交易配置已迁入「<el-link type="primary">企业信息</el-link>」中,请进入企业信息中进行配置
+    </el-alert>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from "vue";
+import { getAccountInfo, refreshAccount } from "@/api/module_payment/invoice/account";
+import type { AccountVO } from "@/api/module_payment/invoice/account";
+
+const account = reactive<AccountVO>({});
+const rechargeExpanded = ref(true);
+
+async function fetchData() {
+  const res = await getAccountInfo();
+  Object.assign(account, res.data.data || {});
+}
+async function refresh() {
+  const res = await refreshAccount();
+  Object.assign(account, res.data.data || {});
+}
+onMounted(fetchData);
+</script>

+ 144 - 0
frontend/src/views/module_payment/invoice/company/index.vue

@@ -0,0 +1,144 @@
+<template>
+  <div class="app-container" v-loading="loading">
+    <div style="display:flex;justify-content:space-between;align-items:center">
+      <h3>企业信息</h3>
+      <div style="display:flex;gap:8px">
+        <el-button @click="handleSync">同步企业税务信息</el-button>
+        <el-button @click="handleValidate">企业信息校验</el-button>
+      </div>
+    </div>
+    <el-card shadow="never" style="margin-top:16px">
+      <template #header><span style="font-weight:600">基本信息</span></template>
+      <el-descriptions :column="2" border size="small">
+        <el-descriptions-item label="企业名称">{{ config.company_name }}</el-descriptions-item>
+        <el-descriptions-item label="企业税号">{{ config.tax_no }}</el-descriptions-item>
+        <el-descriptions-item label="开户行">{{ config.bank_name }}</el-descriptions-item>
+        <el-descriptions-item label="银行账号">{{ config.bank_account }}</el-descriptions-item>
+        <el-descriptions-item label="企业地址" :span="2">{{ config.address }}</el-descriptions-item>
+        <el-descriptions-item label="企业电话">{{ config.phone }}</el-descriptions-item>
+        <el-descriptions-item label="纳税地区">{{ config.tax_region }}</el-descriptions-item>
+        <el-descriptions-item label="开票员">{{ config.issuer_name }} {{ config.issuer_id_card }} <el-button link size="small">修改</el-button></el-descriptions-item>
+      </el-descriptions>
+    </el-card>
+    <el-card shadow="never" style="margin-top:16px">
+      <template #header>
+        <div style="display:flex;justify-content:space-between;align-items:center">
+          <span style="font-weight:600">业务联系人</span>
+          <el-button v-if="!editingContact" link @click="startEditContact">修改信息</el-button>
+          <el-button v-else type="primary" size="small" @click="saveContact">完 成</el-button>
+        </div>
+      </template>
+      <template v-if="!editingContact">
+        <p>业务联系人:{{ config.contact_name }}</p>
+        <p>业务联系电话:{{ config.contact_phone }}</p>
+      </template>
+      <el-form v-else inline>
+        <el-form-item label="业务联系人"><el-input v-model="editContactName" placeholder="请输入姓名" clearable /></el-form-item>
+        <el-form-item label="业务联系电话"><el-input v-model="editContactPhone" placeholder="请输入电话" clearable /></el-form-item>
+      </el-form>
+    </el-card>
+    <el-card shadow="never" style="margin-top:16px">
+      <template #header><span style="font-weight:600">授信额度</span><span style="font-size:12px;color:#909399;margin-left:8px">授信额度数据来源为税局,有疑问请咨询主管税局</span></template>
+      <el-row :gutter="16">
+        <el-col :span="6"><el-statistic title="本月赋额额度(元)" :value="config.monthly_quota || 0" /></el-col>
+        <el-col :span="6"><el-statistic title="可用剩余额度(元)" :value="config.available_quota || 0" /></el-col>
+        <el-col :span="6"><el-statistic title="已下载额度(元)" :value="config.downloaded_quota || 0" /></el-col>
+        <el-col :span="6"><el-statistic title="已用额度(元)" :value="config.used_quota || 0" /></el-col>
+      </el-row>
+    </el-card>
+    <el-card shadow="never" style="margin-top:16px">
+      <template #header>
+        <div style="display:flex;justify-content:space-between;align-items:center">
+          <span style="font-weight:600">开票信息</span>
+          <el-button v-if="!editingInvoice" link @click="startEditInvoice">修 改</el-button>
+          <el-button v-else type="primary" size="small" @click="saveInvoice">完 成</el-button>
+        </div>
+      </template>
+      <el-descriptions :column="2" border size="small">
+        <el-descriptions-item label="计税方式">{{ config.tax_method === 'SIMPLIFIED' ? '简易计税' : config.tax_method }}</el-descriptions-item>
+        <el-descriptions-item label="纳税地区">{{ config.tax_region }}</el-descriptions-item>
+        <el-descriptions-item label="默认票种">
+          <template v-if="!editingInvoice">{{ config.default_invoice_type === 'ORDINARY' ? '普票' : config.default_invoice_type }}</template>
+          <el-select v-else v-model="editDefaultInvoiceType" size="small"><el-option label="普票" value="ORDINARY" /></el-select>
+        </el-descriptions-item>
+        <el-descriptions-item label="默认税率">
+          <template v-if="!editingInvoice">{{ config.default_tax_rate }}</template>
+          <el-select v-else v-model="editDefaultTaxRate" size="small"><el-option label="1%" value="1%" /></el-select>
+        </el-descriptions-item>
+      </el-descriptions>
+    </el-card>
+    <el-card shadow="never" style="margin-top:16px">
+      <template #header><span style="font-weight:600">交易规则设置</span></template>
+      <el-form label-width="300px">
+        <el-form-item label="每笔订单要求完成货品审核后,再付款"><el-switch v-model="config.require_audit_before_pay" /></el-form-item>
+        <el-form-item label="与自然人交易时邀请其成为你的供应商"><el-switch v-model="config.invite_to_supplier" /></el-form-item>
+        <el-form-item label="电子回单收款方名称明文显示"><el-switch v-model="config.show_payee_name" /></el-form-item>
+        <el-form-item label="企业员工额度管理"><el-switch v-model="config.employee_quota_enabled" /></el-form-item>
+      </el-form>
+    </el-card>
+    <el-card shadow="never" style="margin-top:16px">
+      <template #header><span style="font-weight:600">服务商授权管理</span></template>
+      <p>请进入【商家平台-账号中心-授权管理】中 <el-link type="primary" href="#">查看详情</el-link></p>
+      <p style="font-size:12px;color:#909399">网址链接:b.alipay.com/page/sp-account-center/authorize.htm</p>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, onMounted } from "vue";
+import { getCompanyConfig, updateCompanyInfo, updateInvoiceInfo } from "@/api/module_payment/invoice/company";
+import type { CompanyConfigVO } from "@/api/module_payment/invoice/company";
+import { ElMessage } from "element-plus";
+
+const loading = ref(false);
+const editingContact = ref(false);
+const editingInvoice = ref(false);
+const config = reactive<CompanyConfigVO>({});
+const editContactName = ref("");
+const editContactPhone = ref("");
+const editDefaultInvoiceType = ref("ORDINARY");
+const editDefaultTaxRate = ref("1%");
+
+async function fetchData() {
+  loading.value = true;
+  try {
+    const res = await getCompanyConfig();
+    Object.assign(config, res.data.data || {});
+    editContactName.value = config.contact_name || "";
+    editContactPhone.value = config.contact_phone || "";
+    editDefaultInvoiceType.value = config.default_invoice_type || "ORDINARY";
+    editDefaultTaxRate.value = config.default_tax_rate || "1%";
+  } finally { loading.value = false; }
+}
+function handleSync() { ElMessage.info("同步功能即将上线"); }
+function handleValidate() { ElMessage.info("校验功能即将上线"); }
+function startEditContact() {
+  editContactName.value = config.contact_name || "";
+  editContactPhone.value = config.contact_phone || "";
+  editingContact.value = true;
+}
+async function saveContact() {
+  editingContact.value = false;
+  try {
+    await updateCompanyInfo({ contact_name: editContactName.value, contact_phone: editContactPhone.value });
+    config.contact_name = editContactName.value;
+    config.contact_phone = editContactPhone.value;
+    ElMessage.success("保存成功");
+  } catch { ElMessage.error("保存失败"); }
+}
+function startEditInvoice() {
+  editDefaultInvoiceType.value = config.default_invoice_type || "ORDINARY";
+  editDefaultTaxRate.value = config.default_tax_rate || "1%";
+  editingInvoice.value = true;
+}
+async function saveInvoice() {
+  editingInvoice.value = false;
+  try {
+    await updateInvoiceInfo({ default_invoice_type: editDefaultInvoiceType.value, default_tax_rate: editDefaultTaxRate.value });
+    config.default_invoice_type = editDefaultInvoiceType.value;
+    config.default_tax_rate = editDefaultTaxRate.value;
+    ElMessage.success("保存成功");
+  } catch { ElMessage.error("保存失败"); }
+}
+onMounted(fetchData);
+</script>

+ 75 - 0
frontend/src/views/module_payment/invoice/employee/components/EmployeeFormDialog.vue

@@ -0,0 +1,75 @@
+<template>
+  <el-dialog v-model="visible" :title="isEdit ? '编辑员工' : '新增员工'" width="480px" @close="resetForm">
+    <el-form ref="formRef" :model="form" label-width="90px">
+      <el-form-item label="* 角色" prop="role">
+        <el-select v-model="form.role" placeholder="请选择" :disabled="isEdit" style="width:100%">
+          <el-option v-for="o in ROLE_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
+        </el-select>
+      </el-form-item>
+      <el-form-item v-if="form.role" label="* 姓名" prop="name">
+        <el-input v-model="form.name" placeholder="请输入" clearable />
+      </el-form-item>
+      <el-form-item v-if="form.role" label="* 手机号" prop="phone">
+        <el-input v-model="form.phone" placeholder="请输入" clearable />
+      </el-form-item>
+      <el-form-item v-if="form.role === 'CLERK'" label="">
+        <el-checkbox v-model="form.allowSelectSupplier">允许在小程序上选择供应商发送订单</el-checkbox>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button v-if="!isEdit" @click="resetForm">重 置</el-button>
+      <el-button type="primary" @click="handleSave" :disabled="!form.name||!form.role" :loading="saving">保 存</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from "vue";
+import { createEmployee, updateEmployee, ROLE_OPTIONS } from "@/api/module_payment/invoice/employee";
+import type { EmployeeVO } from "@/api/module_payment/invoice/employee";
+import { ElMessage } from "element-plus";
+
+const emit = defineEmits<{ (e: 'success'): void }>();
+const visible = ref(false);
+const isEdit = ref(false);
+const saving = ref(false);
+const editId = ref<number | null>(null);
+
+const form = reactive({ role: '', name: '', phone: '', allowSelectSupplier: false });
+
+function open(row?: EmployeeVO) {
+  if (row) {
+    isEdit.value = true;
+    editId.value = row.id!;
+    form.role = row.role || '';
+    form.name = row.name || '';
+    form.phone = row.phone || '';
+    form.allowSelectSupplier = row.allow_select_supplier || false;
+  } else {
+    isEdit.value = false;
+    editId.value = null;
+    resetForm();
+  }
+  visible.value = true;
+}
+
+function resetForm() { form.role = ''; form.name = ''; form.phone = ''; form.allowSelectSupplier = false; }
+
+async function handleSave() {
+  saving.value = true;
+  try {
+    const data = { role: form.role, name: form.name, phone: form.phone, allow_select_supplier: form.allowSelectSupplier };
+    if (isEdit.value) {
+      await updateEmployee(editId.value!, data);
+    } else {
+      await createEmployee(data);
+    }
+    ElMessage.success(isEdit.value ? '编辑成功' : '新增成功');
+    visible.value = false;
+    emit('success');
+  } catch { ElMessage.error('操作失败'); }
+  finally { saving.value = false; }
+}
+
+defineExpose({ open });
+</script>

+ 72 - 0
frontend/src/views/module_payment/invoice/employee/index.vue

@@ -0,0 +1,72 @@
+<template>
+  <div class="app-container">
+    <h3>员工管理</h3>
+    <el-card shadow="never" style="margin-top:16px">
+      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
+        <span style="font-weight:600">{{ companyName }}</span>
+        <el-button type="primary" @click="handleAdd">新 增</el-button>
+      </div>
+      <el-table :data="tableData" border v-loading="loading">
+        <el-table-column prop="name" label="员工姓名" min-width="140" />
+        <el-table-column prop="phone" label="手机号" min-width="140">
+          <template #default="{ row }">{{ row.phone || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="id_card" label="身份证号" min-width="160">
+          <template #default="{ row }">{{ row.id_card || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="角色" min-width="120">
+          <template #default="{ row }">{{ ROLE_LABEL[row.role] || row.role }}</template>
+        </el-table-column>
+        <el-table-column label="操作" min-width="120" align="center" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="handleEdit(row)" v-if="row.role !== 'SUPER_ADMIN'">编辑</el-button>
+            <el-button type="danger" link @click="handleDelete(row)" v-if="row.role !== 'SUPER_ADMIN'">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination v-model:current-page="searchForm.pageNo" v-model:page-size="searchForm.pageSize" :total="total" layout="total, prev, pager, next" @change="fetchData" style="margin-top:16px;justify-content:flex-end" />
+    </el-card>
+    <EmployeeFormDialog ref="formDialogRef" @success="fetchData" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from "vue";
+import { getEmployeeList, deleteEmployee, ROLE_OPTIONS } from "@/api/module_payment/invoice/employee";
+import type { EmployeeVO } from "@/api/module_payment/invoice/employee";
+import EmployeeFormDialog from "./components/EmployeeFormDialog.vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+
+const ROLE_LABEL: Record<string, string> = Object.fromEntries(ROLE_OPTIONS.map(o => [o.value, o.label]));
+const companyName = "湖南省铭恩商务管理有限公司";
+
+const searchForm = reactive<Record<string, any>>({ pageNo: 1, pageSize: 10 });
+const tableData = ref<EmployeeVO[]>([]);
+const total = ref(0);
+const loading = ref(false);
+const formDialogRef = ref();
+
+async function fetchData() {
+  loading.value = true;
+  try {
+    const res = await getEmployeeList(searchForm);
+    tableData.value = res.data.data?.list || [];
+    total.value = res.data.data?.total || 0;
+  } finally { loading.value = false; }
+}
+
+function handleAdd() { formDialogRef.value?.open(); }
+
+function handleEdit(row: EmployeeVO) { formDialogRef.value?.open(row); }
+
+async function handleDelete(row: EmployeeVO) {
+  try {
+    await ElMessageBox.confirm(`确认删除员工 ${row.name}?`, '提示', { type: 'warning' });
+    await deleteEmployee(row.id!);
+    ElMessage.success('删除成功');
+    fetchData();
+  } catch { /* cancelled */ }
+}
+
+onMounted(fetchData);
+</script>

+ 90 - 0
frontend/src/views/module_payment/invoice/goods/index.vue

@@ -0,0 +1,90 @@
+<template>
+  <div class="app-container">
+    <h3>常用商品管理</h3>
+    <p style="color:#909399;font-size:13px">预设常用商品后,营业员可在小程序中快捷选择该商品</p>
+    <el-row :gutter="16" style="margin-top:16px">
+      <el-col :span="6">
+        <el-card shadow="never">
+          <template #header><span style="font-weight:600">商品分类</span></template>
+          <el-tree :data="categoryTree" :props="{ children: 'children', label: 'name' }" node-key="id" highlight-current @node-click="onCategoryClick" default-expand-all />
+        </el-card>
+      </el-col>
+      <el-col :span="18">
+        <el-card shadow="never">
+          <template #header><span style="font-weight:600">{{ currentCategoryName }}</span></template>
+          <el-table :data="tableData" border v-loading="loading">
+            <el-table-column prop="name" label="商品名称" min-width="150" />
+            <el-table-column prop="unit" label="单位" min-width="80" />
+            <el-table-column prop="spec" label="规格型号" min-width="120" />
+            <el-table-column label="操作" min-width="100" align="center" fixed="right">
+              <template #default="{ row }">
+                <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
+                <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
+              </template>
+            </el-table-column>
+            <template #empty><el-empty description="暂无数据" :image-size="60" /></template>
+          </el-table>
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from "vue";
+import { getCategoryTree, getGoodsList, deleteGoods, createGoods, updateGoods } from "@/api/module_payment/invoice/goods";
+import type { GoodsCategoryVO, GoodsVO } from "@/api/module_payment/invoice/goods";
+import { ElMessage, ElMessageBox } from "element-plus";
+
+const categoryTree = ref<GoodsCategoryVO[]>([]);
+const currentCategoryId = ref<number | null>(null);
+const currentCategoryName = ref("报废产品");
+const tableData = ref<GoodsVO[]>([]);
+const loading = ref(false);
+
+async function fetchCategories() {
+  const res = await getCategoryTree();
+  categoryTree.value = res.data.data || [];
+  if (categoryTree.value.length > 0) {
+    currentCategoryId.value = categoryTree.value[0].id;
+    fetchGoods();
+  }
+}
+
+async function fetchGoods() {
+  if (!currentCategoryId.value) return;
+  loading.value = true;
+  try {
+    const res = await getGoodsList({ pageNo: 1, pageSize: 50, categoryId: currentCategoryId.value });
+    tableData.value = res.data.data?.list || [];
+  } finally { loading.value = false; }
+}
+
+function onCategoryClick(data: GoodsCategoryVO) {
+  currentCategoryId.value = data.id;
+  currentCategoryName.value = data.name;
+  fetchGoods();
+}
+
+async function handleEdit(row: GoodsVO) {
+  try {
+    const { value } = await ElMessageBox.prompt('编辑商品名称', '编辑', { inputValue: row.name });
+    if (value) {
+      await updateGoods(row.id!, { name: value, category_id: row.category_id, unit: row.unit, spec: row.spec });
+      ElMessage.success('编辑成功');
+      fetchGoods();
+    }
+  } catch { /* cancelled */ }
+}
+
+async function handleDelete(row: GoodsVO) {
+  try {
+    await ElMessageBox.confirm(`确认删除商品 ${row.name}?`, '提示', { type: 'warning' });
+    await deleteGoods(row.id!);
+    ElMessage.success('删除成功');
+    fetchGoods();
+  } catch { /* cancelled */ }
+}
+
+onMounted(fetchCategories);
+</script>

+ 59 - 0
frontend/src/views/module_payment/invoice/order/components/BatchImportDialog.vue

@@ -0,0 +1,59 @@
+<template>
+  <el-dialog :model-value="visible" @update:model-value="$emit('update:visible', $event)" title="批量导入" width="520px">
+    <div style="padding:8px 0">
+      <p>批量导入:请先 <el-link type="primary" href="https://mdn.alipayobjects.com/industryinvoice/afts/file/0nuqT7638eYAAAAAQNAAAAgAetcKAQFr?af_fileName=%E5%8F%8D%E5%90%91%E5%BC%80%E7%A5%A8%E8%AE%A2%E5%8D%95%E5%AF%BC%E5%85%A5%E6%A8%A1%E6%9D%BF.xlsx" target="_blank">下载导入模版</el-link></p>
+      <p style="color:#909399;font-size:13px;margin:8px 0">批量导入订单后,订单将推送至供应商自然人的支付宝账户,自然人可以完成后续的缴税、开票和收款</p>
+      <el-upload drag :limit="1" accept=".xlsx,.xls" :before-upload="handleBeforeUpload" :http-request="handleUpload" style="margin-top:16px">
+        <el-icon style="font-size:40px;color:#c0c4cc"><UploadFilled /></el-icon>
+        <div style="margin-top:8px">
+          <p>点击或将文件拖拽到这里上传</p>
+          <p style="font-size:12px;color:#909399">文件类型:.xlsx、xls</p>
+          <p style="font-size:12px;color:#909399">仅支持上传1个文件</p>
+          <p style="font-size:12px;color:#909399">文件大小限制:不得超过 3 MB</p>
+        </div>
+      </el-upload>
+    </div>
+    <template #footer>
+      <el-button @click="$emit('update:visible', false)">取 消</el-button>
+      <el-button type="primary" :disabled="!fileReady" @click="handleConfirm" :loading="uploading">确 定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref } from "vue";
+import { UploadFilled } from "@element-plus/icons-vue";
+import { batchImportOrder } from "@/api/module_payment/invoice/order";
+import { ElMessage } from "element-plus";
+import type { UploadRawFile } from "element-plus";
+
+const props = defineProps<{ visible: boolean }>();
+const emit = defineEmits<{ (e: 'update:visible', v: boolean): void; (e: 'success'): void }>();
+
+const fileReady = ref(false);
+const uploading = ref(false);
+let pendingFile: UploadRawFile | null = null;
+
+function handleBeforeUpload(file: UploadRawFile) {
+  const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || file.type === 'application/vnd.ms-excel';
+  const isLt3M = file.size / 1024 / 1024 < 3;
+  if (!isExcel) { ElMessage.error('仅支持 .xlsx、.xls 格式'); return false; }
+  if (!isLt3M) { ElMessage.error('文件大小不得超过 3 MB'); return false; }
+  pendingFile = file;
+  fileReady.value = true;
+  return false; // prevent auto upload
+}
+
+async function handleUpload() {}
+async function handleConfirm() {
+  if (!pendingFile) return;
+  uploading.value = true;
+  try {
+    await batchImportOrder({ file_url: pendingFile.name });
+    ElMessage.success('导入任务已创建,请在任务中心查看进度');
+    emit('update:visible', false);
+    emit('success');
+  } catch { ElMessage.error('导入失败'); }
+  finally { uploading.value = false; fileReady.value = false; pendingFile = null; }
+}
+</script>

+ 81 - 0
frontend/src/views/module_payment/invoice/order/components/OrderDetailDialog.vue

@@ -0,0 +1,81 @@
+<template>
+  <el-dialog v-model="visible" title="查看订单" width="650px">
+    <div v-if="order" style="padding:0 16px">
+      <el-descriptions :column="3" border size="small">
+        <el-descriptions-item label="交易状态">{{ TRADE_STATUS_LABEL[order.trade_status!] || order.trade_status }}</el-descriptions-item>
+        <el-descriptions-item label="自然人姓名">{{ order.natural_person_name }}</el-descriptions-item>
+        <el-descriptions-item label="手机号码">{{ order.natural_person_phone }}</el-descriptions-item>
+      </el-descriptions>
+
+      <el-table :data="order.items" border size="small" style="margin-top:16px">
+        <el-table-column type="index" label="序号" width="60" />
+        <el-table-column prop="goods_name" label="商品名称" />
+        <el-table-column prop="unit_price" label="单价(元)" width="100" align="right" />
+        <el-table-column prop="quantity" label="数量" width="80" align="right" />
+        <el-table-column label="金额(元)" width="100" align="right">
+          <template #default="{ row }">{{ row.amount != null ? row.amount : '-' }}</template>
+        </el-table-column>
+      </el-table>
+
+      <div style="margin-top:16px;padding:12px;background:#f5f7fa;border-radius:6px">
+        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
+          <span>订单总金额</span>
+          <span style="font-size:18px;font-weight:600;color:#e6a23c">¥{{ order.order_total_amount }}</span>
+          <el-button text size="small" @click="showDetail = !showDetail">明细 <el-icon><ArrowDown v-if="!showDetail" /><ArrowUp v-else /></el-icon></el-button>
+        </div>
+        <div v-if="showDetail">
+          <div v-for="t in order.tax_details" :key="t.id" style="display:flex;justify-content:space-between;padding:4px 0;font-size:13px">
+            <span>{{ t.tax_name }}</span>
+            <span>¥{{ t.tax_amount }}</span>
+          </div>
+          <div style="display:flex;justify-content:space-between;padding:4px 0;font-size:13px">
+            <span>货款金额</span><span>¥{{ order.goods_amount }}</span>
+          </div>
+          <div style="display:flex;justify-content:space-between;padding:4px 0;font-size:13px">
+            <span>已缴税额</span><span>¥{{ order.total_tax_paid }}</span>
+          </div>
+          <div style="display:flex;justify-content:space-between;padding:4px 0;font-size:13px;padding-left:16px">
+            <span>个人所得税</span><span>¥{{ order.personal_income_tax }}</span>
+          </div>
+          <div style="display:flex;justify-content:space-between;padding:4px 0;font-size:13px;padding-left:16px">
+            <span>增值税</span><span>¥{{ order.value_added_tax }}</span>
+          </div>
+          <div style="padding:4px 0;font-size:13px">增值税附加</div>
+          <div style="display:flex;justify-content:space-between;padding:4px 0;font-size:13px;padding-left:16px">
+            <span>城市维护建设税</span><span>¥{{ order.urban_maintenance_tax }}</span>
+          </div>
+          <div style="display:flex;justify-content:space-between;padding:4px 0;font-size:13px;padding-left:16px">
+            <span>教育费附加</span><span>¥{{ order.education_surcharge }}</span>
+          </div>
+          <div style="display:flex;justify-content:space-between;padding:4px 0;font-size:13px;padding-left:16px">
+            <span>地方教育附加</span><span>¥{{ order.local_education_surcharge }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+    <template #footer><el-button @click="visible = false">返 回</el-button></template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref } from "vue";
+import { ArrowDown, ArrowUp } from "@element-plus/icons-vue";
+import { getOrderDetail, TRADE_STATUS_OPTIONS } from "@/api/module_payment/invoice/order";
+import type { OrderVO } from "@/api/module_payment/invoice/order";
+
+const TRADE_STATUS_LABEL: Record<string, string> = Object.fromEntries(TRADE_STATUS_OPTIONS.map(o => [o.value, o.label]));
+
+const visible = ref(false);
+const order = ref<OrderVO | null>(null);
+const showDetail = ref(false);
+
+async function open(orderId: number) {
+  visible.value = true;
+  showDetail.value = false;
+  try {
+    const res = await getOrderDetail(orderId);
+    order.value = res.data.data || null;
+  } catch { order.value = null; }
+}
+defineExpose({ open });
+</script>

+ 44 - 0
frontend/src/views/module_payment/invoice/order/components/OrderInvoiceDialog.vue

@@ -0,0 +1,44 @@
+<template>
+  <el-dialog v-model="visible" title="查看发票" width="700px" @close="handleClose">
+    <div v-if="invoices.length" style="display:flex;gap:24px">
+      <div v-for="inv in invoices" :key="inv.type" style="flex:1">
+        <h4>{{ inv.type === 'RED' ? '红票' : '蓝票' }} <span style="color:#909399;font-size:12px">共1张</span></h4>
+        <div style="border:1px solid #ebeef5;border-radius:6px;padding:16px;text-align:center">
+          <el-image :src="inv.type === 'RED' ? redImg : blueImg" style="width:120px;height:160px" fit="contain">
+            <template #error><div style="width:120px;height:160px;background:#f5f7fa;display:flex;align-items:center;justify-content:center;color:#909399">发票预览</div></template>
+          </el-image>
+          <div style="margin-top:8px">
+            <el-button link><el-icon><View /></el-icon> 预览</el-button>
+          </div>
+          <p style="margin:4px 0;font-size:13px">发票号码:{{ inv.invoice_no }}</p>
+          <p style="margin:4px 0;font-size:13px">含税金额:<span style="color:#e6a23c;font-weight:600">¥{{ inv.tax_amount }}</span></p>
+          <p v-if="inv.red_status" style="margin:4px 0;font-size:13px">红冲状态:{{ inv.red_status === 'RED_SUCCESS' ? '红冲成功' : inv.red_status }}</p>
+          <el-button size="small" style="margin-top:8px">导出</el-button>
+        </div>
+      </div>
+    </div>
+    <template #footer><el-button @click="visible = false">返 回</el-button></template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref } from "vue";
+import { View } from "@element-plus/icons-vue";
+import { getOrderInvoice } from "@/api/module_payment/invoice/order";
+import type { InvoiceVO } from "@/api/module_payment/invoice/order";
+
+const visible = ref(false);
+const invoices = ref<InvoiceVO[]>([]);
+const redImg = "/placeholder-invoice-red.png";
+const blueImg = "/placeholder-invoice-blue.png";
+
+async function open(orderId: number) {
+  visible.value = true;
+  try {
+    const res = await getOrderInvoice(orderId);
+    invoices.value = res.data.data || [];
+  } catch { invoices.value = []; }
+}
+function handleClose() { invoices.value = []; }
+defineExpose({ open });
+</script>

+ 156 - 0
frontend/src/views/module_payment/invoice/order/index.vue

@@ -0,0 +1,156 @@
+<template>
+  <div class="app-container">
+    <el-card shadow="never" class="search-card">
+      <el-form :model="searchForm" inline>
+        <el-form-item label="订单创建时间">
+          <el-date-picker v-model="searchForm.orderTimeRange" type="daterange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD" />
+        </el-form-item>
+        <el-form-item label="营业员姓名">
+          <el-input v-model="searchForm.clerkName" placeholder="请输入" clearable />
+        </el-form-item>
+        <el-form-item label="交易状态">
+          <el-select v-model="searchForm.tradeStatus" placeholder="请选择" clearable>
+            <el-option v-for="o in TRADE_STATUS_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="含税金额(元)">
+          <el-input-number v-model="searchForm.taxAmountMin" :precision="2" :controls="false" placeholder="请输入" />
+          <span style="margin:0 4px">元 -</span>
+          <el-input-number v-model="searchForm.taxAmountMax" :precision="2" :controls="false" placeholder="请输入" />
+          <span style="margin:0 4px">元</span>
+        </el-form-item>
+        <el-form-item label="支付时间">
+          <el-date-picker v-model="searchForm.paymentTimeRange" type="daterange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD" />
+        </el-form-item>
+        <el-form-item label="发票号码">
+          <el-input v-model="searchForm.invoiceNo" placeholder="请输入" clearable />
+        </el-form-item>
+        <el-form-item label="自然人姓名">
+          <el-input v-model="searchForm.naturalPersonName" placeholder="请输入" clearable />
+        </el-form-item>
+        <el-form-item label="收款账号类型">
+          <el-select v-model="searchForm.collectionAccountType" placeholder="请选择" clearable>
+            <el-option v-for="o in ACCOUNT_TYPE_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="handleReset">重 置</el-button>
+          <el-button type="primary" @click="handleQuery">查 询</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+    <el-card shadow="never" style="margin-top:16px">
+      <div style="display:flex;gap:8px;margin-bottom:12px">
+        <el-button type="primary" @click="batchImportVisible = true">批量导入</el-button>
+        <el-button @click="handleBatchCancel" :disabled="selectedIds.length === 0">批量取消</el-button>
+        <el-dropdown @command="handleExport">
+          <el-button>导出 <el-icon><ArrowDown /></el-icon></el-button>
+          <template #dropdown>
+            <el-dropdown-menu>
+              <el-dropdown-item command="order">订单导出</el-dropdown-item>
+              <el-dropdown-item command="invoice">发票导出</el-dropdown-item>
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
+      </div>
+      <el-table :data="tableData" border v-loading="loading" @selection-change="onSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column prop="order_no" label="订单号" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="order_time" label="订单创建时间" min-width="160" />
+        <el-table-column prop="alipay_trade_no" label="支付宝交易号" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="payment_time" label="支付时间" min-width="160" />
+        <el-table-column prop="natural_person_name" label="自然人姓名" min-width="100" />
+        <el-table-column label="收款账号类型" min-width="110">
+          <template #default="{ row }">{{ ACCOUNT_TYPE_LABEL[row.collection_account_type] || row.collection_account_type }}</template>
+        </el-table-column>
+        <el-table-column prop="tax_amount" label="含税金额(元)" min-width="120" align="right" />
+        <el-table-column label="交易状态" min-width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.trade_status === 'SUCCESS' ? 'success' : 'info'">{{ TRADE_STATUS_LABEL[row.trade_status] || row.trade_status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="invoice_no" label="发票号码" min-width="180" show-overflow-tooltip />
+        <el-table-column prop="invoice_pre_tax_amount" label="发票不含税金额(元)" min-width="160" align="right" />
+        <el-table-column prop="invoice_tax_amount" label="发票税额(元)" min-width="120" align="right" />
+        <el-table-column prop="red_invoice_no" label="红字发票号码" min-width="180" show-overflow-tooltip />
+        <el-table-column prop="clerk_name" label="营业员姓名" min-width="100" />
+        <el-table-column label="附件" min-width="80" align="center">
+          <template #default="{ row }">{{ row.attachments ? '有' : '-' }}</template>
+        </el-table-column>
+        <el-table-column label="操作" fixed="right" min-width="160" align="center">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="showInvoice(row)">查看发票</el-button>
+            <el-button type="primary" link @click="showOrder(row)">查看订单</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination v-model:current-page="searchForm.pageNo" v-model:page-size="searchForm.pageSize" :total="total" layout="total, prev, pager, next, sizes" :page-sizes="[10,20,50]" @change="fetchData" style="margin-top:16px;justify-content:flex-end" />
+    </el-card>
+    <OrderInvoiceDialog ref="invoiceDialogRef" />
+    <OrderDetailDialog ref="orderDialogRef" />
+    <BatchImportDialog v-model:visible="batchImportVisible" @success="fetchData" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from "vue";
+import { ArrowDown } from "@element-plus/icons-vue";
+import { getOrderList, batchCancelOrders, exportOrders, exportInvoices, TRADE_STATUS_OPTIONS, ACCOUNT_TYPE_OPTIONS } from "@/api/module_payment/invoice/order";
+import type { OrderVO } from "@/api/module_payment/invoice/order";
+import OrderInvoiceDialog from "./components/OrderInvoiceDialog.vue";
+import OrderDetailDialog from "./components/OrderDetailDialog.vue";
+import BatchImportDialog from "./components/BatchImportDialog.vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+
+const TRADE_STATUS_LABEL: Record<string, string> = Object.fromEntries(TRADE_STATUS_OPTIONS.map(o => [o.value, o.label]));
+const ACCOUNT_TYPE_LABEL: Record<string, string> = Object.fromEntries(ACCOUNT_TYPE_OPTIONS.map(o => [o.value, o.label]));
+
+const searchForm = reactive<Record<string, any>>({ pageNo: 1, pageSize: 10 });
+const tableData = ref<OrderVO[]>([]);
+const total = ref(0);
+const loading = ref(false);
+const selectedIds = ref<number[]>([]);
+const batchImportVisible = ref(false);
+const invoiceDialogRef = ref();
+const orderDialogRef = ref();
+
+function onSelectionChange(rows: OrderVO[]) { selectedIds.value = rows.map(r => r.id!).filter(Boolean) as number[]; }
+
+async function fetchData() {
+  loading.value = true;
+  try {
+    const res = await getOrderList(searchForm);
+    tableData.value = res.data.data?.list || [];
+    total.value = res.data.data?.total || 0;
+  } finally { loading.value = false; }
+}
+
+function handleQuery() { searchForm.pageNo = 1; fetchData(); }
+function handleReset() {
+  const keep = { pageNo: 1, pageSize: searchForm.pageSize };
+  Object.keys(searchForm).forEach(k => { if (!(k in keep)) delete searchForm[k]; });
+  searchForm.pageNo = 1;
+  handleQuery();
+}
+
+function showInvoice(row: OrderVO) { invoiceDialogRef.value?.open(row.id); }
+function showOrder(row: OrderVO) { orderDialogRef.value?.open(row.id); }
+
+async function handleBatchCancel() {
+  try {
+    await ElMessageBox.confirm('确认取消选中的订单?', '提示', { type: 'warning' });
+    await batchCancelOrders(selectedIds.value);
+    ElMessage.success('操作成功');
+    fetchData();
+  } catch { /* cancelled */ }
+}
+
+async function handleExport(type: string) {
+  try {
+    const res = type === 'order' ? await exportOrders() : await exportInvoices();
+    ElMessage.success('导出成功');
+  } catch { ElMessage.error('导出失败'); }
+}
+
+onMounted(fetchData);
+</script>

+ 79 - 0
frontend/src/views/module_payment/invoice/supplier/components/SupplierFormDialog.vue

@@ -0,0 +1,79 @@
+<template>
+  <el-dialog v-model="visible" :title="isEdit ? '编辑客户' : '新增客户'" width="480px" @close="resetForm">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
+      <el-form-item label="* 姓名" prop="name">
+        <el-input v-model="form.name" placeholder="请输入" />
+      </el-form-item>
+      <el-form-item label="* 收款账号类型" prop="accountType">
+        <el-select v-model="form.accountType" placeholder="请选择" style="width:100%">
+          <el-option v-for="o in ACCOUNT_TYPE_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="* 收款账号" prop="accountNo">
+        <el-input v-model="form.accountNo" placeholder="请输入" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="resetForm">重 置</el-button>
+      <el-button type="primary" @click="handleSave" :disabled="!form.name||!form.accountType||!form.accountNo" :loading="saving">保 存</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from "vue";
+import { createSupplier, updateSupplier, SUPPLIER_ACCOUNT_TYPE_OPTIONS } from "@/api/module_payment/invoice/supplier";
+import type { SupplierVO } from "@/api/module_payment/invoice/supplier";
+import { ElMessage } from "element-plus";
+
+const ACCOUNT_TYPE_OPTIONS = SUPPLIER_ACCOUNT_TYPE_OPTIONS;
+const emit = defineEmits<{ (e: 'success'): void }>();
+const visible = ref(false);
+const isEdit = ref(false);
+const saving = ref(false);
+const editId = ref<number | null>(null);
+
+const form = reactive({ name: '', accountType: 'PHONE', accountNo: '' });
+const rules = {
+  name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
+  accountType: [{ required: true, message: '请选择账号类型', trigger: 'change' }],
+  accountNo: [{ required: true, message: '请输入收款账号', trigger: 'blur' }],
+};
+
+function open(row?: SupplierVO) {
+  if (row) {
+    isEdit.value = true;
+    editId.value = row.id!;
+    form.name = row.name || '';
+    form.accountType = row.account_type || 'PHONE';
+    form.accountNo = row.account_no || '';
+  } else {
+    isEdit.value = false;
+    editId.value = null;
+    resetForm();
+  }
+  visible.value = true;
+}
+
+function resetForm() {
+  form.name = ''; form.accountType = 'PHONE'; form.accountNo = '';
+}
+
+async function handleSave() {
+  saving.value = true;
+  try {
+    const data = { name: form.name, account_type: form.accountType, account_no: form.accountNo };
+    if (isEdit.value) {
+      await updateSupplier(editId.value!, data);
+    } else {
+      await createSupplier(data);
+    }
+    ElMessage.success(isEdit.value ? '编辑成功' : '新增成功');
+    visible.value = false;
+    emit('success');
+  } catch { ElMessage.error('操作失败'); }
+  finally { saving.value = false; }
+}
+
+defineExpose({ open });
+</script>

+ 86 - 0
frontend/src/views/module_payment/invoice/supplier/index.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="app-container">
+    <h3>供应商管理</h3>
+    <el-card shadow="never" style="margin-top:16px">
+      <template #header><span style="font-weight:600">供应商信息</span></template>
+      <el-form :model="searchForm" inline>
+        <el-form-item label="姓名"><el-input v-model="searchForm.name" placeholder="请输入" clearable /></el-form-item>
+        <el-form-item label="收款账号"><el-input v-model="searchForm.accountNo" placeholder="请输入" clearable /></el-form-item>
+        <el-form-item label="联系电话"><el-input v-model="searchForm.phone" placeholder="请输入" clearable /></el-form-item>
+        <el-form-item>
+          <el-button @click="handleReset">重 置</el-button>
+          <el-button type="primary" @click="handleQuery">查 询</el-button>
+        </el-form-item>
+      </el-form>
+      <div style="display:flex;gap:8px;margin-bottom:12px">
+        <el-button type="primary" @click="handleBatchImport">批量导入</el-button>
+        <el-button type="primary" @click="handleAdd">新 增</el-button>
+      </div>
+      <el-table :data="tableData" border v-loading="loading">
+        <el-table-column prop="name" label="姓名" min-width="100" />
+        <el-table-column label="账号类型" min-width="120">
+          <template #default="{ row }">{{ ACCOUNT_TYPE_LABEL[row.account_type] || row.account_type }}</template>
+        </el-table-column>
+        <el-table-column prop="account_no" label="收款账号" min-width="140" show-overflow-tooltip />
+        <el-table-column prop="phone" label="联系电话" min-width="120" />
+        <el-table-column label="确认状态" min-width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.confirm_status === 'CONFIRMED' ? 'success' : 'warning'">{{ CONFIRM_LABEL[row.confirm_status] || row.confirm_status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" min-width="120" align="center" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
+            <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination v-model:current-page="searchForm.pageNo" v-model:page-size="searchForm.pageSize" :total="total" layout="total, prev, pager, next, sizes" :page-sizes="[10,20,50]" @change="fetchData" style="margin-top:16px;justify-content:flex-end" />
+    </el-card>
+    <SupplierFormDialog ref="formDialogRef" @success="fetchData" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from "vue";
+import { getSupplierList, deleteSupplier, SUPPLIER_ACCOUNT_TYPE_OPTIONS, CONFIRM_STATUS_OPTIONS } from "@/api/module_payment/invoice/supplier";
+import type { SupplierVO } from "@/api/module_payment/invoice/supplier";
+import SupplierFormDialog from "./components/SupplierFormDialog.vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+
+const ACCOUNT_TYPE_LABEL: Record<string, string> = Object.fromEntries(SUPPLIER_ACCOUNT_TYPE_OPTIONS.map(o => [o.value, o.label]));
+const CONFIRM_LABEL: Record<string, string> = Object.fromEntries(CONFIRM_STATUS_OPTIONS.map(o => [o.value, o.label]));
+
+const searchForm = reactive<Record<string, any>>({ pageNo: 1, pageSize: 10 });
+const tableData = ref<SupplierVO[]>([]);
+const total = ref(0);
+const loading = ref(false);
+const formDialogRef = ref();
+
+async function fetchData() {
+  loading.value = true;
+  try {
+    const res = await getSupplierList(searchForm);
+    tableData.value = res.data.data?.list || [];
+    total.value = res.data.data?.total || 0;
+  } finally { loading.value = false; }
+}
+
+function handleQuery() { searchForm.pageNo = 1; fetchData(); }
+function handleReset() { Object.keys(searchForm).forEach(k => { if (!['pageNo','pageSize'].includes(k)) delete searchForm[k]; }); handleQuery(); }
+
+function handleAdd() { formDialogRef.value?.open(); }
+function handleEdit(row: SupplierVO) { formDialogRef.value?.open(row); }
+
+async function handleDelete(row: SupplierVO) {
+  try {
+    await ElMessageBox.confirm(`确认删除供应商 ${row.name}?`, '提示', { type: 'warning' });
+    await deleteSupplier(row.id!);
+    ElMessage.success('删除成功');
+    fetchData();
+  } catch { /* cancelled */ }
+}
+
+function handleBatchImport() { ElMessage.info('批量导入功能即将上线'); }
+onMounted(fetchData);
+</script>

+ 71 - 0
frontend/src/views/module_payment/invoice/task/index.vue

@@ -0,0 +1,71 @@
+<template>
+  <div class="app-container">
+    <h3>任务中心</h3>
+    <el-card shadow="never" style="margin-top:16px">
+      <template #header><span style="font-weight:600">数据明细</span></template>
+      <el-form :model="searchForm" inline>
+        <el-form-item label="任务开始时间">
+          <el-date-picker v-model="searchForm.timeRange" type="daterange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD" />
+        </el-form-item>
+        <el-form-item label="任务类型">
+          <el-select v-model="searchForm.taskType" placeholder="请选择" clearable>
+            <el-option v-for="o in TASK_TYPE_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="handleReset">重 置</el-button>
+          <el-button type="primary" @click="handleQuery">查 询</el-button>
+        </el-form-item>
+      </el-form>
+      <el-table :data="tableData" border v-loading="loading">
+        <el-table-column prop="start_time" label="任务开始时间" min-width="160" />
+        <el-table-column prop="finish_time" label="任务完成时间" min-width="160" />
+        <el-table-column prop="product" label="所属产品" min-width="120" />
+        <el-table-column label="任务类型" min-width="140">
+          <template #default="{ row }">{{ TASK_TYPE_LABEL[row.task_type] || row.task_type }}</template>
+        </el-table-column>
+        <el-table-column label="状态" min-width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.task_status === 'COMPLETED' ? 'success' : row.task_status === 'FAILED' ? 'danger' : 'warning'">
+              {{ TASK_STATUS_LABEL[row.task_status] || row.task_status }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" min-width="100" align="center">
+          <template #default="{ row }">
+            <el-button v-if="row.file_url" type="primary" link>下载</el-button>
+          </template>
+        </el-table-column>
+        <template #empty><el-empty description="暂无数据" :image-size="80" /></template>
+      </el-table>
+      <el-pagination v-model:current-page="searchForm.pageNo" v-model:page-size="searchForm.pageSize" :total="total" layout="total, prev, pager, next, sizes" :page-sizes="[10,20,50]" @change="fetchData" style="margin-top:16px;justify-content:flex-end" />
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from "vue";
+import { getTaskList, TASK_TYPE_OPTIONS, TASK_STATUS_OPTIONS } from "@/api/module_payment/invoice/task";
+import type { TaskVO } from "@/api/module_payment/invoice/task";
+
+const TASK_TYPE_LABEL: Record<string, string> = Object.fromEntries(TASK_TYPE_OPTIONS.map(o => [o.value, o.label]));
+const TASK_STATUS_LABEL: Record<string, string> = Object.fromEntries(TASK_STATUS_OPTIONS.map(o => [o.value, o.label]));
+
+const searchForm = reactive<Record<string, any>>({ pageNo: 1, pageSize: 10 });
+const tableData = ref<TaskVO[]>([]);
+const total = ref(0);
+const loading = ref(false);
+
+async function fetchData() {
+  loading.value = true;
+  try {
+    const res = await getTaskList(searchForm);
+    tableData.value = res.data.data?.list || [];
+    total.value = res.data.data?.total || 0;
+  } finally { loading.value = false; }
+}
+
+function handleQuery() { searchForm.pageNo = 1; fetchData(); }
+function handleReset() { Object.keys(searchForm).forEach(k => { if (!['pageNo','pageSize'].includes(k)) delete searchForm[k]; }); handleQuery(); }
+onMounted(fetchData);
+</script>

+ 52 - 0
frontend/src/views/module_payment/invoice/tax/index.vue

@@ -0,0 +1,52 @@
+<template>
+  <div class="app-container">
+    <h3>缴税管理</h3>
+    <el-card shadow="never" style="margin-top:16px;max-width:700px">
+      <div v-loading="loading">
+        <el-radio-group v-model="taxMode" @change="handleChange" style="display:flex;flex-direction:column;gap:24px">
+          <el-radio value="PERSONAL" size="large">
+            <span style="font-weight:600;font-size:15px">个人缴税</span>
+            <p style="color:#909399;font-size:13px;margin:4px 0 0">选择个人缴纳时,由自然人在收款时自行缴纳订单产生的增值税/附加税/个税等税费。</p>
+          </el-radio>
+          <el-radio value="ENTERPRISE" size="large">
+            <span style="font-weight:600;font-size:15px">企业代缴</span>
+            <p style="color:#909399;font-size:13px;margin:4px 0 0">选择企业代缴时,由企业代自然人缴纳订单产生的增值税/附加税/个税等税费。特别说明反向开票时计算的个税税率不一定是最终的税率,在自然人个税年度汇算清缴时,自然人可能会产生补/退税的情况、在交易时需要企业与目然人明确告知、具体税款计算规则以税局政策为准。</p>
+            <el-button v-if="!enterpriseEnabled" type="primary" @click.stop="handleOpenEnterprise" style="margin-top:8px">立即开通</el-button>
+          </el-radio>
+        </el-radio-group>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from "vue";
+import { getTaxConfig, updateTaxConfig } from "@/api/module_payment/invoice/tax";
+import { ElMessage } from "element-plus";
+
+const taxMode = ref("PERSONAL");
+const enterpriseEnabled = ref(false);
+const loading = ref(false);
+
+async function fetchConfig() {
+  loading.value = true;
+  try {
+    const res = await getTaxConfig();
+    taxMode.value = res.data.data?.taxMode || "PERSONAL";
+    enterpriseEnabled.value = res.data.data?.enterpriseEnabled || false;
+  } finally { loading.value = false; }
+}
+
+async function handleChange(val: string) {
+  try {
+    await updateTaxConfig({ taxMode: val });
+    ElMessage.success("缴税方式已切换");
+  } catch { ElMessage.error("切换失败"); }
+}
+
+function handleOpenEnterprise() {
+  ElMessage.info("企业代缴开通功能即将上线");
+}
+
+onMounted(fetchConfig);
+</script>

+ 305 - 0
java/.claude/plan/code-review-invoice-platform.md

@@ -0,0 +1,305 @@
+# 行业发票平台 — 代码审查报告
+
+**审查日期**: 2026-07-08  
+**分支**: fapiao  
+**审查范围**: 未提交的全部 83 个文件(SQL ×2、Java ×60、前端 ×21)
+
+---
+
+## 概览
+
+| 类别 | 文件数 | 说明 |
+|------|--------|------|
+| SQL DDL | 1 (`012_invoice_tables.sql`) | 10 张表,结构完整 |
+| SQL DML | 1 (`013_invoice_menu.sql`) | 1 父菜单 + 8 子菜单 |
+| Java Entity | 10 | MyBatis-Plus 实体,继承基类正确 |
+| Java Mapper | 10 | 空接口,未接入 DB |
+| Java DTO/VO | 20 | 含校验注解 |
+| Java Controller | 8 | REST API,路由完整 |
+| Java Service | 8 | **全部为内存 Mock 实现** |
+| Java Enum | 4 | 含中文 label |
+| 前端 API TS | 8 | axios 封装,类型定义完整 |
+| 前端 Vue 页面 | 7 | Element Plus |
+| 前端 Vue 组件 | 4 | Dialog 弹窗类 |
+| 前端 Mock TS | 1 | 与后端 mock 数据镜像 |
+| 核心修改 | 1 (`TenantInnerInterceptor`) | 新增 2 张条件表 |
+
+---
+
+## 🔴 严重问题(必须修复)
+
+### 问题 1:JSON 字段命名前后端全量不匹配
+
+**位置**: 全部前端 API 文件 + 全部后端 Controller 返回值
+
+后端 Java 默认序列化为 camelCase:
+```json
+{ "orderNo": "xxx", "tradeStatus": "SUCCESS", "alipayTradeNo": "yyy" }
+```
+
+前端所有 TS 接口定义使用 snake_case:
+```typescript
+interface OrderVO {
+  order_no?: string;
+  trade_status?: string;
+  alipay_trade_no?: string;
+}
+```
+
+**影响**: 所有 API 返回的数据,前端无法正确读取。`res.data.data?.list` 中每个对象的属性全部为 `undefined`。
+
+**修复方案(二选一)**:
+
+方案 A — 后端统一配置(推荐,改动最小):
+```yaml
+# application.yml
+spring:
+  jackson:
+    property-naming-strategy: SNAKE_CASE
+```
+
+方案 B — 前端接口定义全部改为 camelCase,同步修改所有 `.vue` 文件中的模板绑定。
+
+---
+
+### 问题 2:商品编辑不调用后端 API
+
+**文件**: `frontend/src/views/module_payment/invoice/goods/index.vue`  
+**行号**: 69-77
+
+```typescript
+async function handleEdit(row: GoodsVO) {
+  try {
+    const { value } = await ElMessageBox.prompt('编辑商品名称', '编辑', { inputValue: row.name });
+    if (value) {
+      row.name = value;  // ← 直接改了本地变量,从未调 updateGoods API
+      ElMessage.success('编辑成功');
+    }
+  } catch { /* cancelled */ }
+}
+```
+
+**影响**: 用户编辑商品后,刷新页面数据丢失。后端从未收到更新请求。
+
+**修复方案**:
+```typescript
+async function handleEdit(row: GoodsVO) {
+  try {
+    const { value } = await ElMessageBox.prompt('编辑商品名称', '编辑', { inputValue: row.name });
+    if (value) {
+      await updateGoods(row.id!, { name: value, category_id: row.category_id });
+      ElMessage.success('编辑成功');
+      fetchGoods();
+    }
+  } catch { /* cancelled */ }
+}
+```
+
+---
+
+### 问题 3:员工表单提交字段名与后端不一致
+
+**文件**: `frontend/src/views/module_payment/invoice/employee/components/EmployeeFormDialog.vue`  
+**行号**: 62-64
+
+```typescript
+// form 使用 camelCase
+const form = reactive({ role: '', name: '', phone: '', allowSelectSupplier: false });
+// 直接 spread 提交
+await createEmployee({ ...form });
+```
+
+如果执行了问题 1 的修复(后端改为 snake_case),则 `allowSelectSupplier` 需要改为 `allow_select_supplier`。
+
+**修复方案**: 统一命名策略后,确保前端提交的字段名与后端 `@RequestBody` 期望一致。使用 Jackson snake_case 时,前端提交的 JSON key 也必须用 snake_case。
+
+---
+
+## 🟡 中等问题(应该修复)
+
+### 问题 4:`TaskQueryDTO` 分页字段类型错误
+
+**文件**: `java/src/main/java/com/payment/platform/module/payment/invoice/task/dto/TaskQueryDTO.java`
+
+```java
+private String pageNo = "1";    // ← 应该是 Integer
+private String pageSize = "10"; // ← 应该是 Integer
+```
+
+而 `TaskService.list()` 中手动做了 `Integer.parseInt()`。其他所有模块的 QueryDTO 都使用 `Integer` 类型。
+
+**修复方案**: 改为 `Integer`,与其他模块保持一致,删除 `parseInt` 调用。
+
+---
+
+### 问题 5:`TaxService.currentMode` 非线程安全
+
+**文件**: `java/src/main/java/com/payment/platform/module/payment/invoice/tax/service/TaxService.java`  
+**行号**: 11
+
+```java
+private String currentMode = "PERSONAL";  // 实例字段,多用户共享
+```
+
+Spring Service 默认是 singleton,所有请求共享同一个实例。用户 A 切换为 ENTERPRISE 后,用户 B 读到的也是 ENTERPRISE。
+
+**影响**: 多用户并发时,缴税模式会互相覆盖。
+
+**修复方案**: 后续接入数据库时,将 tax_mode 存到 `pay_invoice_company_config` 表(DDL 中已有 `tax_method` 字段),按 enterprise_id 隔离。
+
+---
+
+### 问题 6:全部 Service 是 Mock 实现,10 个 Mapper 未被使用
+
+**文件**: 全部 `*Service.java` + 全部 `*Mapper.java`
+
+- 所有数据存储在 `ConcurrentHashMap` 中
+- 10 个 `@Mapper` 接口定义了 `BaseMapper<T>` 但从未被 `@Autowired` 注入到 Service
+- Entity 上的 `@TableName` 注解从未被验证过是否能正确映射到数据库表
+
+**说明**: 原型阶段可以接受 mock 实现,但以下工作需要在接入真实 DB 前完成:
+- 验证 Entity 字段名与数据库列名的映射(MyBatis-Plus 默认 camelCase → snake_case)
+- 验证 `@TableName` 对应的表确实存在于数据库中
+- Service 注入 Mapper 替换 `ConcurrentHashMap`
+
+---
+
+### 问题 7:`OrderService.initMockData()` 金额逻辑需要注释
+
+**文件**: `java/src/main/java/com/payment/platform/module/payment/invoice/order/service/OrderService.java`  
+**行号**: 27-92
+
+Mock 数据中:
+- `goodsAmount = 9.98` + `totalTaxPaid = 0.02` = `orderTotalAmount = 10.00` ✓
+- `totalTaxPaid = 0.02` = `personalIncomeTax = 0.02` + 增值税 0 + 附加税 0 ✓
+- 但 `taxAmount = 10.00`(含税金额),`invoicePreTaxAmount = 9.90`(不含税), `invoiceTaxAmount = 0.10`(发票税额)
+
+发票维度的计算(9.90 + 0.10 = 10.00)与订单维度(9.98 + 0.02 = 10.00)对不上——货物金额在发票上显示为 9.90,但在订单计税时显示为 9.98。这可能反映了真实业务中发票金额与计税基础的差异(个税不进发票),但缺乏注释说明。
+
+**修复方案**: 在 `initMockData()` 方法上加注释,解释金额之间的业务关系。
+
+---
+
+### 问题 8:所有 Controller 定义 `currentUser()` 但未调用
+
+**文件**: 全部 8 个 Controller
+
+每个 Controller 都有:
+```java
+private LoginUser currentUser() {
+    Authentication a = SecurityContextHolder.getContext().getAuthentication();
+    if (a != null && a.getPrincipal() instanceof LoginUser u) return u;
+    return null;
+}
+```
+
+但没有任何一个方法调用它。当前缺少:
+- 租户 ID 注入(`tenant_id` 字段未被填充到 entity)
+- 企业 ID 注入(`enterprise_id` 字段未被填充到 entity)
+- 操作权限校验
+
+**修复方案**: 在 create/update 方法中调用 `currentUser()` 获取 `tenantId`,填充到 entity;或者通过 MyBatis-Plus 的 `@TableField(fill = FieldFill.INSERT)` + MetaObjectHandler 自动填充(base entity 已有此注解)。
+
+---
+
+## 🟢 建议改进
+
+### 9. 前端 `mock/` 目录未被激活
+
+**文件**: `frontend/src/mock/module_payment/invoice/index.ts`
+
+Mock 数据函数定义完整,但没有发现 `vite-plugin-mock` 或 MSW 的配置来拦截请求。当前前端直接调用后端 API(后端返回 mock 数据),这个文件是死代码。
+
+**建议**: 要么接入 mock 插件让前端可独立开发,要么删除此文件避免误导。
+
+---
+
+### 10. 发票占位图片不存在
+
+**文件**: `frontend/src/views/module_payment/invoice/order/components/OrderInvoiceDialog.vue`  
+**行号**: 32-33
+
+```typescript
+const redImg = "/placeholder-invoice-red.png";
+const blueImg = "/placeholder-invoice-blue.png";
+```
+
+这两个图片文件在项目中不存在,`<el-image>` 会触发 error slot 显示占位文字。页面功能正常但视觉效果打折扣。
+
+---
+
+### 11. SQL DDL 缺少外键约束
+
+`pay_invoice_order_item.order_id` 和 `pay_invoice_tax_detail.order_id` 没有 `FOREIGN KEY` 约束。虽然应用层可保证一致性,但缺少数据库层面的引用完整性保护。
+
+---
+
+### 12. Entity 字段过多
+
+`OrderEntity` 包含 25+ 个业务字段(交易信息 + 发票信息 + 税务信息混在一张表)。后续建议拆分:
+
+```
+pay_invoice_order          → 交易基础信息
+pay_invoice_order_tax      → 各税种金额(已有 tax_detail,但主表仍有冗余汇总字段)
+```
+
+当前设计是一次性把支付宝回调的所有字段落了宽表,短期可用,长期维护成本高。
+
+---
+
+### 13. TenantInnerInterceptor 修改审查
+
+**文件**: `java/src/main/java/com/payment/platform/core/tenant/TenantInnerInterceptor.java`  
+**修改内容**: CONDITIONAL_TABLES 集合中新增了 `pay_invoice_order` 和 `pay_invoice_task`
+
+**审查结论**: ✅ 修改逻辑正确。
+
+- 已认证用户访问这两张表 → 应用租户过滤 ✓
+- 定时任务/导出回调等无认证场景访问 → 跳过租户过滤 ✓
+
+**需要注意**: 以下发票表未加入 CONDITIONAL_TABLES,意味着它们始终应用租户过滤(无认证场景会查不到数据):
+- `pay_invoice_supplier`
+- `pay_invoice_employee`
+- `pay_invoice_goods`
+- `pay_invoice_goods_category`
+- `pay_invoice_company_config`
+- `pay_invoice_transfer_account`
+- `pay_invoice_order_item`
+- `pay_invoice_tax_detail`
+
+如果这些表只由登录用户在 UI 中操作,当前配置是正确的。如果后续有定时任务需要扫这些表,需要把对应的表名加入 CONDITIONAL_TABLES。
+
+---
+
+## 优先级执行清单
+
+| 优先级 | 问题 | 修复点 |
+|--------|------|--------|
+| 🔴 P0 | JSON 命名不匹配 | 后端加 `jackson.property-naming-strategy: SNAKE_CASE`,或前端全部改用 camelCase |
+| 🔴 P0 | 商品编辑不调 API | `goods/index.vue` 的 `handleEdit` 调用 `updateGoods` |
+| 🔴 P0 | 表单提交字段统一 | 确认前端提交 JSON key 与后端匹配 |
+| 🟡 P1 | TaskQueryDTO 类型 | `pageNo`/`pageSize` 改为 `Integer` |
+| 🟡 P1 | TaxService 线程安全 | `currentMode` 按 enterprise 隔离或存 DB |
+| 🟡 P1 | Mock 金额加注释 | 解释 9.98 vs 9.90 的业务含义 |
+| 🟡 P1 | Controller 注入租户/企业 ID | create/update 时填充 tenantId、enterpriseId |
+| 🟢 P2 | Mapper 接入 DB | Service 注入 Mapper 替换 ConcurrentHashMap |
+| 🟢 P2 | 发票占位图 | 上传实际图片或替换为 CSS 占位 |
+| 🟢 P2 | Mock 文件 | 接入 vite-plugin-mock 或删除 |
+| 🟢 P2 | SQL 外键 | 加 FOREIGN KEY 约束 |
+
+---
+
+## 架构评价
+
+**做得好的地方**:
+- 模块划分清晰:order / supplier / employee / goods / company / task / account / tax 各司其职
+- 代码风格一致:所有 Controller 同一模式,所有 Service 同一结构
+- 枚举值完备且带中文 label
+- Entity 继承基类正确:有 enterprise_id 的用 `PaymentEnterpriseBaseEntity`,只有 tenant_id 的用 `PaymentTenantBaseEntity`
+- `TenantInnerInterceptor` 的修改最小化且逻辑正确
+- 前端 Vue 组件化合理,Dialog 组件可复用
+
+**需要关注的风险点**:
+- Mock → 真实 DB 的切换工作量被低估(需验证所有 `@TableName`、字段映射、分页查询、事务)
+- JSON 命名策略统一是阻塞性前置条件
+- 缺少任何形式的自动化测试

+ 682 - 0
java/.claude/plan/industry-invoice-platform.md

@@ -0,0 +1,682 @@
+# Implementation Plan: Industry Invoice Platform (行业发票平台)
+
+**Date:** 2026-07-08
+**Branch:** fapiao
+**Status:** Planning
+
+---
+
+## Overview
+
+Copy the Alipay industry invoice platform (https://fxkp.alipay.com/industry-invoice-platform/) as a new module in the payment platform. 8 pages with full CRUD, import/export, and mock APIs. Multi-tenant + enterprise + admin data scoping on all tables.
+
+---
+
+## 1. Database Tables
+
+All tables use `PaymentEnterpriseBaseEntity` (inherits `PaymentTenantBaseEntity` → `PaymentBaseEntity`), providing: `id` (snowflake), `status`, `description`, `created_time`, `updated_time`, `tenant_id`, `enterprise_id`.
+
+Flyway migration file: `V1.2__create_pay_invoice_tables.sql`
+
+### 1.1 `pay_invoice_order` — 交易及发票订单
+
+| Column | Type | Constraint | Notes |
+|---|---|---|---|
+| id | BIGINT | PK (snowflake) | From PaymentEnterpriseBaseEntity |
+| status | VARCHAR(10) | DEFAULT '0' | From PaymentEnterpriseBaseEntity |
+| description | TEXT | | From PaymentEnterpriseBaseEntity |
+| created_time | TIMESTAMP | NOT NULL DEFAULT NOW() | From PaymentEnterpriseBaseEntity |
+| updated_time | TIMESTAMP | NOT NULL DEFAULT NOW() | From PaymentEnterpriseBaseEntity |
+| tenant_id | BIGINT | | From PaymentTenantBaseEntity |
+| enterprise_id | VARCHAR(64) | | From PaymentEnterpriseBaseEntity |
+| order_no | VARCHAR(64) | NOT NULL | 订单编号 |
+| alipay_trade_no | VARCHAR(64) | | 支付宝交易号 |
+| payment_time | TIMESTAMP | | 支付时间 |
+| natural_person_name | VARCHAR(64) | | 自然人姓名 |
+| collection_account_type | VARCHAR(32) | | 收款账户类型: ALIPAY / BANK_CARD |
+| tax_inclusive_amount | DECIMAL(18,2) | | 含税金额 |
+| trade_status | VARCHAR(32) | NOT NULL DEFAULT 'PENDING_ASSOCIATE' | 交易状态 |
+| invoice_no | VARCHAR(64) | | 发票号码 |
+| invoice_pretax_amount | DECIMAL(18,2) | | 发票税前金额 |
+| invoice_tax_amount | DECIMAL(18,2) | | 发票税额 |
+| red_invoice_no | VARCHAR(64) | | 红票号码 |
+| clerk_name | VARCHAR(64) | | 营业员姓名 |
+| attachment_url | TEXT | | 附件URL JSON array |
+
+Indexes:
+- `idx_invoice_order_enterprise` ON `(enterprise_id)`
+- `idx_invoice_order_order_no` ON `(order_no)`
+- `idx_invoice_order_trade_no` ON `(alipay_trade_no)`
+- `idx_invoice_order_trade_status` ON `(trade_status)`
+- `idx_invoice_order_created_time` ON `(created_time)`
+
+### 1.2 `pay_invoice_supplier` — 供应商
+
+| Column | Type | Constraint | Notes |
+|---|---|---|---|
+| id | BIGINT | PK (snowflake) | |
+| status, description, created_time, updated_time, tenant_id, enterprise_id | | | From base entities |
+| supplier_name | VARCHAR(128) | NOT NULL | 供应商名称 |
+| account_type | VARCHAR(32) | NOT NULL | ALIPAY_PHONE / ALIPAY_EMAIL |
+| collection_account | VARCHAR(128) | NOT NULL | 收款账户 |
+| phone | VARCHAR(20) | | 联系电话 |
+| confirm_status | VARCHAR(32) | NOT NULL DEFAULT 'PENDING' | CONFIRMED / PENDING / REJECTED |
+
+Indexes:
+- `idx_invoice_supplier_enterprise` ON `(enterprise_id)`
+- `idx_invoice_supplier_name` ON `(supplier_name)`
+- `idx_invoice_supplier_confirm_status` ON `(confirm_status)`
+
+### 1.3 `pay_invoice_employee` — 员工管理
+
+| Column | Type | Constraint | Notes |
+|---|---|---|---|
+| id | BIGINT | PK (snowflake) | |
+| status, description, created_time, updated_time, tenant_id, enterprise_id | | | From base entities |
+| employee_name | VARCHAR(64) | NOT NULL | 员工姓名 |
+| phone | VARCHAR(20) | NOT NULL | 手机号 |
+| id_number | VARCHAR(20) | NOT NULL | 身份证号 |
+| role | VARCHAR(32) | NOT NULL | CLERK / INVOICER / SUPER_ADMIN |
+| allow_miniapp_supplier | BOOLEAN | DEFAULT FALSE | 允许小程序选供应商 |
+
+Indexes:
+- `idx_invoice_employee_enterprise` ON `(enterprise_id)`
+- `idx_invoice_employee_role` ON `(role)`
+
+### 1.4 `pay_invoice_product` — 常用商品
+
+| Column | Type | Constraint | Notes |
+|---|---|---|---|
+| id | BIGINT | PK (snowflake) | |
+| status, description, created_time, updated_time, tenant_id, enterprise_id | | | From base entities |
+| product_name | VARCHAR(128) | NOT NULL | 商品名称 |
+| category | VARCHAR(32) | NOT NULL | 报废产品分类 |
+| unit | VARCHAR(32) | | 单位 |
+| spec | VARCHAR(128) | | 规格 |
+
+Indexes:
+- `idx_invoice_product_enterprise` ON `(enterprise_id)`
+- `idx_invoice_product_category` ON `(category)`
+
+### 1.5 `pay_invoice_company` — 企业信息
+
+| Column | Type | Constraint | Notes |
+|---|---|---|---|
+| id | BIGINT | PK (snowflake) | |
+| status, description, created_time, updated_time, tenant_id, enterprise_id | | | From base entities |
+| company_name | VARCHAR(128) | | 企业名称 |
+| tax_id | VARCHAR(64) | | 税号 |
+| bank_name | VARCHAR(128) | | 开户银行 |
+| bank_account | VARCHAR(64) | | 银行账号 |
+| address | VARCHAR(256) | | 地址 |
+| phone | VARCHAR(20) | | 电话 |
+| tax_region | VARCHAR(64) | | 纳税地区 |
+| issuer | VARCHAR(64) | | 开票人 |
+| contact_name | VARCHAR(64) | | 业务联系人姓名 |
+| contact_phone | VARCHAR(20) | | 业务联系人电话 |
+| monthly_quota | DECIMAL(18,2) | DEFAULT 10000000 | 月度授信额度 |
+| available_quota | DECIMAL(18,2) | | 可用额度 |
+| downloaded_count | INTEGER | DEFAULT 0 | 已下载张数 |
+| used_count | INTEGER | DEFAULT 0 | 已使用张数 |
+| tax_calculation_method | VARCHAR(32) | DEFAULT 'SIMPLE' | 计税方式 |
+| default_invoice_type | VARCHAR(32) | DEFAULT 'NORMAL' | 默认票种 |
+| default_tax_rate | VARCHAR(16) | DEFAULT '1%' | 默认税率 |
+| audit_after_payment | BOOLEAN | DEFAULT TRUE | 货品审核后付款 |
+| invite_as_supplier | BOOLEAN | DEFAULT FALSE | 邀请为供应商 |
+| receipt_plaintext | BOOLEAN | DEFAULT FALSE | 电子回单明文 |
+| employee_quota_management | BOOLEAN | DEFAULT FALSE | 员工额度管理 |
+
+Index:
+- `idx_invoice_company_enterprise` ON `(enterprise_id)` UNIQUE (one record per enterprise)
+
+### 1.6 `pay_invoice_task` — 任务中心
+
+| Column | Type | Constraint | Notes |
+|---|---|---|---|
+| id | BIGINT | PK (snowflake) | |
+| status, description, created_time, updated_time, tenant_id, enterprise_id | | | From base entities |
+| task_type | VARCHAR(32) | NOT NULL | 任务类型 |
+| task_status | VARCHAR(32) | NOT NULL DEFAULT 'PENDING' | PENDING / PROCESSING / SUCCESS / FAILED |
+| start_time | TIMESTAMP | | 任务开始时间 |
+| completion_time | TIMESTAMP | | 完成时间 |
+| product_name | VARCHAR(128) | | 商品名称 |
+| file_url | VARCHAR(512) | | 导出文件URL |
+| total_count | INTEGER | DEFAULT 0 | 总记录数 |
+| success_count | INTEGER | DEFAULT 0 | 成功数 |
+| fail_count | INTEGER | DEFAULT 0 | 失败数 |
+| error_message | TEXT | | 错误信息 |
+
+Indexes:
+- `idx_invoice_task_enterprise` ON `(enterprise_id)`
+- `idx_invoice_task_type` ON `(task_type)`
+- `idx_invoice_task_status` ON `(task_status)`
+
+### 1.7 `pay_invoice_tax_payment` — 缴税记录
+
+| Column | Type | Constraint | Notes |
+|---|---|---|---|
+| id | BIGINT | PK (snowflake) | |
+| status, description, created_time, updated_time, tenant_id, enterprise_id | | | From base entities |
+| payment_mode | VARCHAR(32) | NOT NULL | INDIVIDUAL / ENTERPRISE_PROXY |
+| tax_amount | DECIMAL(18,2) | | 缴税金额 |
+| tax_period | VARCHAR(32) | | 纳税期间 |
+| payment_time | TIMESTAMP | | 缴税时间 |
+| payment_status | VARCHAR(32) | DEFAULT 'UNPAID' | UNPAID / PAID |
+| enterprise_proxy_enabled | BOOLEAN | DEFAULT FALSE | 是否开通企业代缴 |
+
+Indexes:
+- `idx_invoice_tax_enterprise` ON `(enterprise_id)`
+- `idx_invoice_tax_status` ON `(payment_status)`
+
+---
+
+## 2. Backend Module Structure
+
+```
+module/payment/invoice/
+├── InvoiceModuleConfig.java          (optional: module-level @Configuration if needed)
+
+├── order/                             # 1. 交易及发票管理
+│   ├── controller/
+│   │   └── OrderController.java
+│   ├── dto/
+│   │   ├── OrderQueryDTO.java
+│   │   ├── OrderVO.java
+│   │   ├── OrderBatchImportDTO.java
+│   │   ├── OrderBatchCancelDTO.java
+│   │   └── InvoiceDetailVO.java
+│   ├── entity/
+│   │   └── OrderEntity.java
+│   ├── enums/
+│   │   └── OrderEnums.java            (TradeStatus, CollectionAccountType)
+│   ├── mapper/
+│   │   └── OrderMapper.java
+│   └── service/
+│       ├── OrderService.java          (list, detail, batchImport, batchCancel, export)
+│       └── impl/
+│           └── OrderServiceImpl.java  (optional — if following existing pattern)
+
+├── account/                           # 2. 转账账户管理
+│   ├── controller/
+│   │   └── InvoiceAccountController.java
+│   ├── dto/
+│   │   └── AccountBalanceVO.java
+│   ├── entity/
+│   │   └── InvoiceAccountEntity.java
+│   ├── mapper/
+│   │   └── InvoiceAccountMapper.java
+│   └── service/
+│       └── InvoiceAccountService.java
+
+├── tax/                               # 3. 缴税管理
+│   ├── controller/
+│   │   └── TaxPaymentController.java
+│   ├── dto/
+│   │   ├── TaxPaymentQueryDTO.java
+│   │   └── TaxPaymentVO.java
+│   ├── entity/
+│   │   └── TaxPaymentEntity.java
+│   ├── enums/
+│   │   └── TaxEnums.java              (PaymentMode, PaymentStatus)
+│   ├── mapper/
+│   │   └── TaxPaymentMapper.java
+│   └── service/
+│       └── TaxPaymentService.java
+
+├── employee/                          # 4. 员工管理
+│   ├── controller/
+│   │   └── InvoiceEmployeeController.java
+│   ├── dto/
+│   │   ├── EmployeeSaveDTO.java
+│   │   └── EmployeeVO.java
+│   ├── entity/
+│   │   └── InvoiceEmployeeEntity.java
+│   ├── enums/
+│   │   └── InvoiceEmployeeEnums.java   (EmployeeRole)
+│   ├── mapper/
+│   │   └── InvoiceEmployeeMapper.java
+│   └── service/
+│       └── InvoiceEmployeeService.java
+
+├── product/                           # 5. 常用商品管理
+│   ├── controller/
+│   │   └── ProductController.java
+│   ├── dto/
+│   │   ├── ProductSaveDTO.java
+│   │   └── ProductVO.java
+│   ├── entity/
+│   │   └── ProductEntity.java
+│   ├── enums/
+│   │   └── ProductEnums.java          (ProductCategory)
+│   ├── mapper/
+│   │   └── ProductMapper.java
+│   └── service/
+│       └── ProductService.java
+
+├── company/                           # 6. 企业信息
+│   ├── controller/
+│   │   └── CompanyController.java
+│   ├── dto/
+│   │   ├── CompanyVO.java
+│   │   ├── ContactUpdateDTO.java
+│   │   └── InvoiceInfoUpdateDTO.java
+│   ├── entity/
+│   │   └── CompanyEntity.java
+│   ├── enums/
+│   │   └── CompanyEnums.java          (TaxCalcMethod, DefaultInvoiceType)
+│   ├── mapper/
+│   │   └── CompanyMapper.java
+│   └── service/
+│       └── CompanyService.java
+
+├── supplier/                          # 7. 供应商管理
+│   ├── controller/
+│   │   └── SupplierController.java
+│   ├── dto/
+│   │   ├── SupplierSaveDTO.java
+│   │   ├── SupplierQueryDTO.java
+│   │   └── SupplierVO.java
+│   ├── entity/
+│   │   └── SupplierEntity.java
+│   ├── enums/
+│   │   └── SupplierEnums.java         (AccountType, ConfirmStatus)
+│   ├── mapper/
+│   │   └── SupplierMapper.java
+│   └── service/
+│       └── SupplierService.java
+
+└── task/                              # 8. 任务中心
+    ├── controller/
+    │   └── TaskController.java
+    ├── dto/
+    │   ├── TaskQueryDTO.java
+    │   └── TaskVO.java
+    ├── entity/
+    │   └── TaskEntity.java
+    ├── enums/
+    │   └── TaskEnums.java             (TaskType, TaskStatus)
+    ├── mapper/
+    │   └── TaskMapper.java
+    ├── scheduler/
+    │   └── InvoiceTaskScheduler.java   (poll export/import tasks, execute asynchronously)
+    └── service/
+        ├── TaskService.java
+        └── InvoiceMockDataService.java (singleton mock data factory for all modules)
+```
+
+**Key design decisions:**
+- Controllers: `@RestController` with `@RequestMapping("/payment/invoice/<submodule>")`
+- Services: `@Service @RequiredArgsConstructor` using `LambdaQueryWrapper` for queries
+- Mappers: `@Mapper extends BaseMapper<Entity>`
+- All entities extend `PaymentEnterpriseBaseEntity` (auto-tenant + auto-enterprise)
+- `InvoiceMockDataService` generates realistic mock data using Faker-like patterns for all modules
+- PageResult wrapping: `PageResult.of(pageNo, pageSize, total, items)`
+
+---
+
+## 3. API Endpoints (Mock Implementations)
+
+### 3.1 Order Controller
+```
+GET    /payment/invoice/order/list           — paginated list with filters
+GET    /payment/invoice/order/{id}           — order detail
+GET    /payment/invoice/order/invoice/{id}   — invoice detail (红票/蓝票)
+POST   /payment/invoice/order/batch-import   — batch import (multipart .xlsx)
+POST   /payment/invoice/order/batch-cancel   — batch cancel by order IDs
+GET    /payment/invoice/order/export/orders  — export orders as .xlsx
+GET    /payment/invoice/order/export/invoices — export invoices as .xlsx
+GET    /payment/invoice/order/template       — download import template
+```
+
+### 3.2 Account Controller
+```
+GET    /payment/invoice/account/balance      — get account balance + pending amount
+GET    /payment/invoice/account/recharge-info — get recharge bank account info
+POST   /payment/invoice/account/recharge     — (mock) recharge
+```
+
+### 3.3 Tax Payment Controller
+```
+GET    /payment/invoice/tax/config            — get tax payment config (mode, proxy enabled)
+POST   /payment/invoice/tax/mode              — set payment mode (individual / enterprise)
+POST   /payment/invoice/tax/enable-proxy      — enable enterprise proxy payment
+GET    /payment/invoice/tax/records           — tax payment history
+```
+
+### 3.4 Employee Controller
+```
+GET    /payment/invoice/employee/list         — employee list
+POST   /payment/invoice/employee              — add employee
+PUT    /payment/invoice/employee/{id}         — update employee
+DELETE /payment/invoice/employee/{id}         — delete employee
+```
+
+### 3.5 Product Controller
+```
+GET    /payment/invoice/product/list          — product list (filter by category)
+POST   /payment/invoice/product               — add product
+PUT    /payment/invoice/product/{id}          — update product
+DELETE /payment/invoice/product/{id}          — delete product
+GET    /payment/invoice/product/categories    — category tree
+```
+
+### 3.6 Company Controller
+```
+GET    /payment/invoice/company/info                — get company info (auto-create if missing)
+POST   /payment/invoice/company/sync-tax            — mock sync tax info
+POST   /payment/invoice/company/verify              — mock company verification
+PUT    /payment/invoice/company/contact             — update contact info
+PUT    /payment/invoice/company/invoice-info        — update invoice settings
+PUT    /payment/invoice/company/trade-rules         — update trade rule toggles
+```
+
+### 3.7 Supplier Controller
+```
+GET    /payment/invoice/supplier/list          — paginated list with search filters
+POST   /payment/invoice/supplier               — add supplier
+PUT    /payment/invoice/supplier/{id}          — update supplier
+DELETE /payment/invoice/supplier/{id}          — delete supplier
+POST   /payment/invoice/supplier/batch-import  — batch import (.xlsx)
+GET    /payment/invoice/supplier/template      — download import template
+```
+
+### 3.8 Task Controller
+```
+GET    /payment/invoice/task/list              — paginated task list
+GET    /payment/invoice/task/{id}              — task detail
+GET    /payment/invoice/task/{id}/download     — download export file
+```
+
+### Mock Data Strategy
+
+`InvoiceMockDataService` will be injected into each service constructor. It generates:
+
+- **Orders:** 50+ records with realistic Chinese names, amounts, statuses distributed across all 8 statuses
+- **Employees:** 5-10 records with masked phone/ID
+- **Products:** 20+ products distributed across all 9 categories
+- **Company:** 1 record per enterprise with hardcoded defaults (10M quota)
+- **Suppliers:** 15+ records with various account types and confirm statuses
+- **Tasks:** 10+ historical tasks with various types and statuses
+- **Account:** Fixed balance values
+
+Each service method checks if real data exists; if not, falls back to mock data.
+
+---
+
+## 4. Frontend Component Structure
+
+```
+src/views/module_payment/invoice/
+├── order/
+│   ├── index.vue                      # main page with search + table
+│   ├── components/
+│   │   ├── SearchForm.vue             # search filters
+│   │   ├── OrderTable.vue             # data table with actions
+│   │   ├── OrderDetailDialog.vue      # order detail modal
+│   │   ├── InvoiceDetailDialog.vue    # invoice detail (红票/蓝票) modal
+│   │   └── BatchImportDialog.vue      # batch import with template download + upload
+│   └── index.ts                       # (if needed for local types)
+
+├── account/
+│   ├── index.vue                      # balance display + recharge section
+│   └── components/
+│       ├── BalanceCard.vue            # account balance + pending amount
+│       └── RechargeGuide.vue          # collapsible "how to recharge" section
+
+├── payTaxes/
+│   ├── index.vue                      # tax payment mode selection
+│   └── components/
+│       ├── IndividualPayment.vue      # 个人缴税 view
+│       └── EnterpriseProxy.vue        # 企业代缴 view with "立即开通" flow
+
+├── employeeManagement/
+│   ├── index.vue                      # employee list + add/edit dialogs
+│   └── components/
+│       ├── EmployeeTable.vue          # table with masked fields
+│       ├── AddEmployeeDialog.vue      # role selector + dynamic form
+│       └── EditEmployeeDialog.vue     # role readonly, edit form
+
+├── goods/
+│   ├── index.vue                      # left category tree + right product table
+│   └── components/
+│       ├── CategoryTree.vue           # 9-category scrapped product tree
+│       └── ProductTable.vue           # product list by selected category
+
+├── company/
+│   ├── index.vue                      # enterprise info dashboard
+│   └── components/
+│       ├── BasicInfoSection.vue       # 8 read-only fields
+│       ├── ContactSection.vue         # editable contact
+│       ├── CreditLimitSection.vue     # 授信额度 4 fields
+│       ├── InvoiceInfoSection.vue     # editable invoice settings
+│       ├── TradeRuleSection.vue       # 4 toggle switches
+│       └── ServiceProviderSection.vue # 服务商授权管理 link
+
+├── suppliers/
+│   ├── index.vue                      # supplier list with search + batch import
+│   └── components/
+│       ├── SupplierTable.vue          # table with confirm_status filter
+│       ├── AddSupplierDialog.vue      # add/edit with save+next
+│       └── BatchImportSupplier.vue    # batch import dialog
+
+└── tasks/
+    ├── index.vue                      # task center with search
+    └── components/
+        ├── TaskTable.vue              # task list
+        └── TaskDetailDrawer.vue       # task detail side panel
+```
+
+API modules:
+```
+src/api/module_payment/invoice/
+├── order.ts
+├── account.ts
+├── tax.ts
+├── employee.ts
+├── product.ts
+├── company.ts
+├── supplier.ts
+└── task.ts
+```
+
+---
+
+## 5. Menu Tree Structure (sys_menu table)
+
+Insert SQL in a migration or seed file. Menu entries reference the dynamic component paths that the frontend `transformRoutes` resolves via `import.meta.glob("../../views/**/**.vue")`.
+
+```
+Industry Invoice Platform (parent menu)
+├── type: 1 (directory), route_path: /invoice-platform, route_name: InvoicePlatform
+│   component_path: (empty, Layout handles it), icon: "invoice", order: 500
+
+├── 交易及发票管理 (type: 2 menu)
+│   parent_id: <parent>, route_path: /invoice-platform/order
+│   route_name: InvoiceOrder, component_path: module_payment/invoice/order/index
+│   permission: module_payment:invoice:order:list
+
+├── 转账账户管理
+│   parent_id: <parent>, route_path: /invoice-platform/account
+│   route_name: InvoiceAccount, component_path: module_payment/invoice/account/index
+│   permission: module_payment:invoice:account:view
+
+├── 缴税管理
+│   parent_id: <parent>, route_path: /invoice-platform/payTaxes
+│   route_name: InvoicePayTaxes, component_path: module_payment/invoice/payTaxes/index
+│   permission: module_payment:invoice:tax:view
+
+├── 员工管理
+│   parent_id: <parent>, route_path: /invoice-platform/employee
+│   route_name: InvoiceEmployee, component_path: module_payment/invoice/employeeManagement/index
+│   permission: module_payment:invoice:employee:list
+
+├── 常用商品管理
+│   parent_id: <parent>, route_path: /invoice-platform/goods
+│   route_name: InvoiceGoods, component_path: module_payment/invoice/goods/index
+│   permission: module_payment:invoice:product:list
+
+├── 企业信息
+│   parent_id: <parent>, route_path: /invoice-platform/company
+│   route_name: InvoiceCompany, component_path: module_payment/invoice/company/index
+│   permission: module_payment:invoice:company:view
+
+├── 供应商管理
+│   parent_id: <parent>, route_path: /invoice-platform/suppliers
+│   route_name: InvoiceSuppliers, component_path: module_payment/invoice/suppliers/index
+│   permission: module_payment:invoice:supplier:list
+
+└── 任务中心
+    parent_id: <parent>, route_path: /invoice-platform/tasks
+    route_name: InvoiceTasks, component_path: module_payment/invoice/tasks/index
+    permission: module_payment:invoice:task:list
+```
+
+**Important:** The `component_path` values must match the file structure `module_payment/invoice/<folder>/index.vue`, because the permission store's `transformRoutes` resolves these with `modules[`../../views/${normalizedRoute.component}.vue`]`.
+
+---
+
+## 6. Implementation Order (7 phases)
+
+### Phase 1: Database + Entities (Foundation)
+1. Create `V1.2__create_pay_invoice_tables.sql` with all 7 tables + indexes
+2. Run migration
+3. Create all entity classes extending `PaymentEnterpriseBaseEntity`
+4. Create all enum classes
+5. Create all mapper interfaces
+
+### Phase 2: Mock Service + Core Backend
+1. Implement `InvoiceMockDataService` with mock data generation
+2. Implement `CompanyService` — single-record per enterprise, auto-create on first access
+3. Implement `OrderService` — list, detail, batch cancel, export
+4. Implement `SupplierService` — list, CRUD, batch import
+5. Implement `InvoiceEmployeeService` — CRUD
+6. Implement `ProductService` — CRUD + categories
+7. Implement `InvoiceAccountService` — balance query
+8. Implement `TaxPaymentService` — config + history
+9. Implement `TaskService` — list, detail
+
+### Phase 3: Backend Controllers
+1. Create all DTOs (VO + QueryDTO + SaveDTO)
+2. Create all controllers with `@PreAuthorize` annotations matching menu permissions
+3. Wire endpoints to services
+
+### Phase 4: Frontend - API Layer + Foundation
+1. Create `src/api/module_payment/invoice/` with all 8 API modules
+2. Set up type definitions for request/response shapes
+
+### Phase 5: Frontend - Simple Pages (bottom-up)
+Implement pages in order of complexity:
+1. **任务中心** — simplest (table + filters, no CRUD)
+2. **供应商管理** — simple CRUD table
+3. **常用商品管理** — left tree + right table
+4. **员工管理** — CRUD with dynamic form
+5. **缴税管理** — radio toggle + button
+6. **转账账户管理** — cards + collapsible section
+
+### Phase 6: Frontend - Complex Pages
+7. **企业信息** — multi-section dashboard with edit modes
+8. **交易及发票管理** — most complex (search, table, batch ops, export, detail dialogs)
+
+### Phase 7: Menu Config + Integration
+1. Insert `sys_menu` records
+2. Test navigation and route generation
+3. Test tenant/enterprise scoping (verify mock data returns different records per tenant/enterprise)
+4. Full integration smoke test
+
+---
+
+## 7. Critical Design Notes
+
+### 7.1 Tenant Isolation
+All tables have `tenant_id` + `enterprise_id` columns. `TenantInnerInterceptor` auto-injects `tenant_id` on INSERT and auto-filters on SELECT. The `enterprise_id` must be set manually in the service layer when creating records (typically from the request context or current enterprise selection).
+
+**Mock data handling:** `InvoiceMockDataService` should accept `tenantId` and `enterpriseId` parameters so mock data is scoped correctly. Each enterprise sees only their own records.
+
+### 7.2 Batch Import Flow
+1. Frontend: download template (`GET /template` returns a pre-formatted .xlsx)
+2. Frontend: upload file (`POST /batch-import` multipart)
+3. Backend: parse .xlsx with Apache POI (already in project dependencies), validate rows, insert to DB
+4. Create a task record in `pay_invoice_task` for tracking
+5. Return success/fail counts
+
+### 7.3 Export Flow
+1. Frontend: click export triggers `GET /export/orders` or `GET /export/invoices`
+2. Backend: generates .xlsx using `ExcelUtil`, saves to temp storage
+3. Create a task record in `pay_invoice_task` with `task_status = 'SUCCESS'`
+4. Return download URL in task detail
+
+### 7.4 Scheduler (InvoiceTaskScheduler)
+A `@Scheduled` job that polls `pay_invoice_task` for `PENDING` tasks and processes them asynchronously. This handles:
+- Batch import parsing in background (for large files)
+- Export file generation
+- Cleanup of old/stale tasks
+
+### 7.5 Frontend Multi-tenant
+The frontend already has an `enterprise.store.ts` for enterprise selection. All API calls should pass `enterprise_id` as a parameter (the backend's `PaymentEnterpriseBaseEntity` handles filtering). The current enterprise is available via `useEnterpriseStore()`.
+
+### 7.6 Mock vs Real Switch
+Each service method should follow this pattern:
+```java
+// In production, real API calls would go here
+// For now, return mock data
+return invoiceMockDataService.getOrders(tenantId, enterpriseId, queryDTO);
+```
+This makes future migration to real APIs straightforward — just replace the mock service call with a real API client call.
+
+### 7.7 File Upload Size Limit
+Configure Spring multipart max size to 3MB for batch import (matches the Alipay constraint):
+```yaml
+spring.servlet.multipart.max-file-size: 3MB
+spring.servlet.multipart.max-request-size: 3MB
+```
+But use a controller-level check to allow larger uploads elsewhere.
+
+---
+
+## 8. Files to Create (Total: ~65 files)
+
+### Backend (~40 files)
+| Layer | Files |
+|---|---|
+| SQL Migration | 1 file |
+| Entities | 7 files |
+| Enums | 7 files |
+| Mappers | 8 files |
+| DTOs | ~15 files |
+| Services | 9 files (8 services + 1 mock service) |
+| Controllers | 8 files |
+| Scheduler | 1 file |
+
+### Frontend (~25 files)
+| Layer | Files |
+|---|---|
+| API modules | 8 files |
+| View pages | 8 files |
+| Components | ~12 files |
+
+---
+
+## 9. Risks & Mitigations
+
+| Risk | Mitigation |
+|---|---|
+| Dynamic route resolution fails for new view paths | Verify `component_path` in sys_menu matches actual file path relative to `src/views/` |
+| Tenant interceptor not working for invoice tables | Tables use `tenant_id` column like existing `pay_*` tables — automatically picked up |
+| `enterprise_id` is String not Long (existing convention) | Use `String enterpriseId` consistently; `InvoiceMockDataService` uses String parameter |
+| Red/Blue invoice display not well-defined | Mock it: red invoice = negative amounts + red color; blue = normal |
+| Large batch import ~3MB causing timeout | Use task scheduler for async processing; immediate response with task ID |
+
+---
+
+## 10. Estimated Effort
+
+| Phase | Effort (hours) |
+|---|---|
+| Phase 1: DB + Entities | 3-4 |
+| Phase 2: Mock + Core Services | 6-8 |
+| Phase 3: Controllers + DTOs | 4-5 |
+| Phase 4: Frontend API Layer | 2-3 |
+| Phase 5: Simple Pages | 8-10 |
+| Phase 6: Complex Pages | 8-10 |
+| Phase 7: Menu + Integration | 2-4 |
+| **Total** | **33-44 hours** |

+ 237 - 0
java/sql/012_invoice_tables.sql

@@ -0,0 +1,237 @@
+-- 行业发票平台 — 全部表 DDL
+-- 反向开票/报废产品收购 模块
+
+-- 1. 交易订单
+CREATE TABLE IF NOT EXISTS public.pay_invoice_order (
+    id              BIGINT PRIMARY KEY,
+    status          VARCHAR(20),
+    description     TEXT,
+    created_time    TIMESTAMPTZ NOT NULL,
+    updated_time    TIMESTAMPTZ NOT NULL,
+    tenant_id       BIGINT NOT NULL,
+    enterprise_id   VARCHAR(64) NOT NULL,
+
+    order_no                VARCHAR(64) NOT NULL,
+    alipay_trade_no         VARCHAR(64),
+    order_time              TIMESTAMPTZ,
+    payment_time            TIMESTAMPTZ,
+    natural_person_name     VARCHAR(64),
+    natural_person_phone    VARCHAR(20),
+    collection_account_type VARCHAR(20),
+    collection_account      VARCHAR(64),
+    tax_amount              DECIMAL(18,2) DEFAULT 0,
+    trade_status            VARCHAR(20),
+    invoice_no              VARCHAR(64),
+    invoice_pre_tax_amount  DECIMAL(18,2),
+    invoice_tax_amount      DECIMAL(18,2),
+    red_invoice_no          VARCHAR(64),
+    clerk_name              VARCHAR(64),
+    attachments             TEXT,
+    product_code            VARCHAR(64),
+    order_total_amount      DECIMAL(18,2),
+    goods_amount            DECIMAL(18,2),
+    total_tax_paid          DECIMAL(18,2),
+    personal_income_tax     DECIMAL(18,2),
+    value_added_tax         DECIMAL(18,2),
+    urban_maintenance_tax   DECIMAL(18,2),
+    education_surcharge     DECIMAL(18,2),
+    local_education_surcharge DECIMAL(18,2)
+);
+CREATE UNIQUE INDEX IF NOT EXISTS idx_invoice_order_no ON public.pay_invoice_order(tenant_id, order_no);
+CREATE INDEX IF NOT EXISTS idx_invoice_order_trade_status ON public.pay_invoice_order(tenant_id, trade_status);
+CREATE INDEX IF NOT EXISTS idx_invoice_order_time ON public.pay_invoice_order(tenant_id, order_time);
+COMMENT ON TABLE public.pay_invoice_order IS '行业发票-交易订单';
+
+-- 2. 订单商品明细
+CREATE TABLE IF NOT EXISTS public.pay_invoice_order_item (
+    id              BIGINT PRIMARY KEY,
+    status          VARCHAR(20),
+    description     TEXT,
+    created_time    TIMESTAMPTZ NOT NULL,
+    updated_time    TIMESTAMPTZ NOT NULL,
+    tenant_id       BIGINT NOT NULL,
+    enterprise_id   VARCHAR(64) NOT NULL,
+
+    order_id        BIGINT NOT NULL,
+    seq_no          INT DEFAULT 1,
+    goods_name      VARCHAR(128),
+    unit_price      DECIMAL(18,4),
+    quantity        DECIMAL(18,4),
+    amount          DECIMAL(18,2)
+);
+CREATE INDEX IF NOT EXISTS idx_invoice_order_item_order ON public.pay_invoice_order_item(order_id);
+COMMENT ON TABLE public.pay_invoice_order_item IS '行业发票-订单商品明细';
+
+-- 3. 订单税额明细
+CREATE TABLE IF NOT EXISTS public.pay_invoice_tax_detail (
+    id              BIGINT PRIMARY KEY,
+    status          VARCHAR(20),
+    description     TEXT,
+    created_time    TIMESTAMPTZ NOT NULL,
+    updated_time    TIMESTAMPTZ NOT NULL,
+    tenant_id       BIGINT NOT NULL,
+    enterprise_id   VARCHAR(64) NOT NULL,
+
+    order_id        BIGINT NOT NULL,
+    tax_type        VARCHAR(32),
+    tax_name        VARCHAR(64),
+    tax_amount      DECIMAL(18,2)
+);
+COMMENT ON TABLE public.pay_invoice_tax_detail IS '行业发票-订单税额明细';
+
+-- 4. 供应商
+CREATE TABLE IF NOT EXISTS public.pay_invoice_supplier (
+    id              BIGINT PRIMARY KEY,
+    status          VARCHAR(20),
+    description     TEXT,
+    created_time    TIMESTAMPTZ NOT NULL,
+    updated_time    TIMESTAMPTZ NOT NULL,
+    tenant_id       BIGINT NOT NULL,
+    enterprise_id   VARCHAR(64) NOT NULL,
+
+    name            VARCHAR(64) NOT NULL,
+    account_type    VARCHAR(20) NOT NULL,
+    account_no      VARCHAR(100) NOT NULL,
+    phone           VARCHAR(20),
+    confirm_status  VARCHAR(20) DEFAULT 'PENDING'
+);
+CREATE UNIQUE INDEX IF NOT EXISTS idx_invoice_supplier_account ON public.pay_invoice_supplier(tenant_id, enterprise_id, account_type, account_no);
+COMMENT ON TABLE public.pay_invoice_supplier IS '行业发票-供应商';
+
+-- 5. 员工
+CREATE TABLE IF NOT EXISTS public.pay_invoice_employee (
+    id              BIGINT PRIMARY KEY,
+    status          VARCHAR(20),
+    description     TEXT,
+    created_time    TIMESTAMPTZ NOT NULL,
+    updated_time    TIMESTAMPTZ NOT NULL,
+    tenant_id       BIGINT NOT NULL,
+
+    name                    VARCHAR(64) NOT NULL,
+    phone                   VARCHAR(20),
+    id_card                 VARCHAR(20),
+    role                    VARCHAR(20) NOT NULL DEFAULT 'CLERK',
+    allow_select_supplier   BOOLEAN DEFAULT FALSE,
+    sys_user_id             BIGINT
+);
+COMMENT ON TABLE public.pay_invoice_employee IS '行业发票-员工';
+
+-- 6. 常用商品
+CREATE TABLE IF NOT EXISTS public.pay_invoice_goods (
+    id              BIGINT PRIMARY KEY,
+    status          VARCHAR(20),
+    description     TEXT,
+    created_time    TIMESTAMPTZ NOT NULL,
+    updated_time    TIMESTAMPTZ NOT NULL,
+    tenant_id       BIGINT NOT NULL,
+
+    category_id     BIGINT NOT NULL,
+    name            VARCHAR(128) NOT NULL,
+    unit            VARCHAR(32),
+    spec            VARCHAR(64),
+    enterprise_id   VARCHAR(64)
+);
+CREATE INDEX IF NOT EXISTS idx_invoice_goods_cat ON public.pay_invoice_goods(tenant_id, category_id);
+COMMENT ON TABLE public.pay_invoice_goods IS '行业发票-常用商品';
+
+-- 7. 商品分类
+CREATE TABLE IF NOT EXISTS public.pay_invoice_goods_category (
+    id              BIGINT PRIMARY KEY,
+    status          VARCHAR(20),
+    description     TEXT,
+    created_time    TIMESTAMPTZ NOT NULL,
+    updated_time    TIMESTAMPTZ NOT NULL,
+    tenant_id       BIGINT NOT NULL,
+
+    parent_id       BIGINT DEFAULT 0,
+    name            VARCHAR(128) NOT NULL,
+    sort_order      INT DEFAULT 0,
+    product_code    VARCHAR(64)
+);
+COMMENT ON TABLE public.pay_invoice_goods_category IS '行业发票-商品分类';
+
+-- 8. 企业配置
+CREATE TABLE IF NOT EXISTS public.pay_invoice_company_config (
+    id              BIGINT PRIMARY KEY,
+    status          VARCHAR(20),
+    description     TEXT,
+    created_time    TIMESTAMPTZ NOT NULL,
+    updated_time    TIMESTAMPTZ NOT NULL,
+    tenant_id       BIGINT NOT NULL,
+    enterprise_id   VARCHAR(64) NOT NULL,
+
+    company_name            VARCHAR(128),
+    tax_no                  VARCHAR(32),
+    bank_name               VARCHAR(128),
+    bank_account            VARCHAR(64),
+    address                 VARCHAR(256),
+    phone                   VARCHAR(32),
+    tax_region              VARCHAR(128),
+    issuer_name             VARCHAR(64),
+    issuer_id_card          VARCHAR(20),
+
+    contact_name            VARCHAR(64),
+    contact_phone           VARCHAR(20),
+
+    monthly_quota           DECIMAL(18,2) DEFAULT 0,
+    available_quota         DECIMAL(18,2) DEFAULT 0,
+    downloaded_quota        DECIMAL(18,2) DEFAULT 0,
+    used_quota              DECIMAL(18,2) DEFAULT 0,
+
+    tax_method              VARCHAR(32) DEFAULT 'SIMPLIFIED',
+    default_invoice_type    VARCHAR(20) DEFAULT 'ORDINARY',
+    default_tax_rate        VARCHAR(10) DEFAULT '1%',
+
+    require_audit_before_pay    BOOLEAN DEFAULT FALSE,
+    invite_to_supplier          BOOLEAN DEFAULT FALSE,
+    show_payee_name             BOOLEAN DEFAULT FALSE,
+    employee_quota_enabled      BOOLEAN DEFAULT FALSE
+);
+CREATE UNIQUE INDEX IF NOT EXISTS idx_invoice_company_config_ent ON public.pay_invoice_company_config(tenant_id, enterprise_id);
+COMMENT ON TABLE public.pay_invoice_company_config IS '行业发票-企业配置';
+
+-- 9. 任务记录
+CREATE TABLE IF NOT EXISTS public.pay_invoice_task (
+    id              BIGINT PRIMARY KEY,
+    status          VARCHAR(20),
+    description     TEXT,
+    created_time    TIMESTAMPTZ NOT NULL,
+    updated_time    TIMESTAMPTZ NOT NULL,
+    tenant_id       BIGINT NOT NULL,
+    enterprise_id   VARCHAR(64) NOT NULL,
+
+    start_time      TIMESTAMPTZ,
+    finish_time     TIMESTAMPTZ,
+    product         VARCHAR(64),
+    task_type       VARCHAR(32),
+    task_status     VARCHAR(20),
+    file_url        VARCHAR(256),
+    total_count     INT,
+    success_count   INT,
+    fail_count      INT,
+    error_msg       TEXT
+);
+CREATE INDEX IF NOT EXISTS idx_invoice_task_type ON public.pay_invoice_task(tenant_id, task_type);
+CREATE INDEX IF NOT EXISTS idx_invoice_task_time ON public.pay_invoice_task(tenant_id, start_time);
+COMMENT ON TABLE public.pay_invoice_task IS '行业发票-任务记录';
+
+-- 10. 转账账户
+CREATE TABLE IF NOT EXISTS public.pay_invoice_transfer_account (
+    id              BIGINT PRIMARY KEY,
+    status          VARCHAR(20),
+    description     TEXT,
+    created_time    TIMESTAMPTZ NOT NULL,
+    updated_time    TIMESTAMPTZ NOT NULL,
+    tenant_id       BIGINT NOT NULL,
+    enterprise_id   VARCHAR(64) NOT NULL,
+
+    total_amount        DECIMAL(18,2) DEFAULT 0,
+    pending_amount      DECIMAL(18,2) DEFAULT 0,
+    bank_account_name   VARCHAR(128),
+    bank_account_no     VARCHAR(64),
+    bank_name           VARCHAR(128),
+    bank_branch         VARCHAR(128),
+    bank_location       VARCHAR(64),
+    bank_code           VARCHAR(32)
+);
+COMMENT ON TABLE public.pay_invoice_transfer_account IS '行业发票-转账账户';

+ 48 - 0
java/sql/013_invoice_menu.sql

@@ -0,0 +1,48 @@
+-- 行业发票平台 — 菜单配置
+-- 一级目录 (parent_id 后续更新为实际插入的id)
+-- type: 0=目录, 1=菜单, 2=按钮
+
+-- 一级目录
+INSERT INTO sys_menu (name, type, "order", icon, route_name, route_path, component_path, title, hidden, keep_alive, always_show, parent_id, status, created_time, updated_time)
+VALUES ('Invoice', 0, 6, 'file-text', 'Invoice', '/industry-invoice-platform', NULL, '行业发票平台', FALSE, FALSE, TRUE, 0, '1', NOW(), NOW());
+
+-- 二级菜单 (parent_id 需为上面那条的 id,此处用子查询)
+DO $$
+DECLARE
+    parent_id bigint;
+BEGIN
+    SELECT id INTO parent_id FROM sys_menu WHERE route_name = 'Invoice' AND parent_id = 0;
+
+    -- 交易及发票管理
+    INSERT INTO sys_menu (name, type, "order", icon, route_name, route_path, component_path, title, hidden, keep_alive, always_show, parent_id, status, created_time, updated_time)
+    VALUES ('InvoiceOrder', 1, 1, 'file-text', 'InvoiceOrder', '/industry-invoice-platform/order', 'module_payment/invoice/order/index', '交易及发票管理', FALSE, TRUE, FALSE, parent_id, '1', NOW(), NOW());
+
+    -- 转账账户管理
+    INSERT INTO sys_menu (name, type, "order", icon, route_name, route_path, component_path, title, hidden, keep_alive, always_show, parent_id, status, created_time, updated_time)
+    VALUES ('InvoiceAccount', 1, 2, '转账', 'InvoiceAccount', '/industry-invoice-platform/account', 'module_payment/invoice/account/index', '转账账户管理', FALSE, TRUE, FALSE, parent_id, '1', NOW(), NOW());
+
+    -- 缴税管理
+    INSERT INTO sys_menu (name, type, "order", icon, route_name, route_path, component_path, title, hidden, keep_alive, always_show, parent_id, status, created_time, updated_time)
+    VALUES ('InvoiceTax', 1, 3, 'file-text', 'InvoiceTax', '/industry-invoice-platform/payTaxes', 'module_payment/invoice/tax/index', '缴税管理', FALSE, TRUE, FALSE, parent_id, '1', NOW(), NOW());
+
+    -- 员工管理
+    INSERT INTO sys_menu (name, type, "order", icon, route_name, route_path, component_path, title, hidden, keep_alive, always_show, parent_id, status, created_time, updated_time)
+    VALUES ('InvoiceEmployee', 1, 4, '员工管理', 'InvoiceEmployee', '/industry-invoice-platform/employeeManagement', 'module_payment/invoice/employee/index', '员工管理', FALSE, TRUE, FALSE, parent_id, '1', NOW(), NOW());
+
+    -- 常用商品管理
+    INSERT INTO sys_menu (name, type, "order", icon, route_name, route_path, component_path, title, hidden, keep_alive, always_show, parent_id, status, created_time, updated_time)
+    VALUES ('InvoiceGoods', 1, 5, '商品2', 'InvoiceGoods', '/industry-invoice-platform/goods', 'module_payment/invoice/goods/index', '常用商品管理', FALSE, TRUE, FALSE, parent_id, '1', NOW(), NOW());
+
+    -- 企业信息
+    INSERT INTO sys_menu (name, type, "order", icon, route_name, route_path, component_path, title, hidden, keep_alive, always_show, parent_id, status, created_time, updated_time)
+    VALUES ('InvoiceCompany', 1, 6, '企业信息', 'InvoiceCompany', '/industry-invoice-platform/company', 'module_payment/invoice/company/index', '企业信息', FALSE, TRUE, FALSE, parent_id, '1', NOW(), NOW());
+
+    -- 供应商管理
+    INSERT INTO sys_menu (name, type, "order", icon, route_name, route_path, component_path, title, hidden, keep_alive, always_show, parent_id, status, created_time, updated_time)
+    VALUES ('InvoiceSupplier', 1, 7, '供应商', 'InvoiceSupplier', '/industry-invoice-platform/suppliers', 'module_payment/invoice/supplier/index', '供应商管理', FALSE, TRUE, FALSE, parent_id, '1', NOW(), NOW());
+
+    -- 任务中心
+    INSERT INTO sys_menu (name, type, "order", icon, route_name, route_path, component_path, title, hidden, keep_alive, always_show, parent_id, status, created_time, updated_time)
+    VALUES ('InvoiceTask', 1, 8, 'KHCFDC_任务中心', 'InvoiceTask', '/industry-invoice-platform/tasks', 'module_payment/invoice/task/index', '任务中心', FALSE, TRUE, FALSE, parent_id, '1', NOW(), NOW());
+
+END $$;

+ 3 - 1
java/src/main/java/com/payment/platform/core/tenant/TenantInnerInterceptor.java

@@ -63,7 +63,9 @@ public class TenantInnerInterceptor extends TenantLineInnerInterceptor {
             "pay_expense_quota",     // 费控额度(通知处理无认证上下文)
             "pay_expense_institution", // 费控制度(通知处理无认证上下文)
             "pay_facetoface_order",  // 当面付申请单(定时轮询无认证上下文)
-            "pay_f2f_trade"          // 当面付收款记录(定时轮询无认证上下文)
+            "pay_f2f_trade",         // 当面付收款记录(定时轮询无认证上下文)
+            "pay_invoice_order",     // 行业发票-交易订单(定时任务/导出回调无认证上下文)
+            "pay_invoice_task"       // 行业发票-任务记录(定时任务无认证上下文)
     );
 
     public TenantInnerInterceptor() {

+ 34 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/account/controller/AccountController.java

@@ -0,0 +1,34 @@
+package com.payment.platform.module.payment.invoice.account.controller;
+
+import com.payment.platform.common.response.Result;
+import com.payment.platform.core.security.LoginUser;
+import com.payment.platform.module.payment.invoice.account.dto.AccountVO;
+import com.payment.platform.module.payment.invoice.account.service.TransferAccountService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/payment/invoice/account")
+@RequiredArgsConstructor
+public class AccountController {
+
+    private final TransferAccountService transferAccountService;
+
+    private LoginUser currentUser() {
+        Authentication a = SecurityContextHolder.getContext().getAuthentication();
+        if (a != null && a.getPrincipal() instanceof LoginUser u) return u;
+        return null;
+    }
+
+    @GetMapping
+    public Result<AccountVO> info() {
+        return Result.ok(transferAccountService.info());
+    }
+
+    @PutMapping("/refresh")
+    public Result<AccountVO> refresh() {
+        return Result.ok(transferAccountService.refresh());
+    }
+}

+ 23 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/account/dto/AccountVO.java

@@ -0,0 +1,23 @@
+package com.payment.platform.module.payment.invoice.account.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.OffsetDateTime;
+
+@Data
+public class AccountVO implements Serializable {
+    private Long id;
+    private BigDecimal totalAmount;
+    private BigDecimal pendingAmount;
+    private String bankAccountName;
+    private String bankAccountNo;
+    private String bankName;
+    private String bankBranch;
+    private String bankLocation;
+    private String bankCode;
+    private String status;
+    private OffsetDateTime createdTime;
+    private OffsetDateTime updatedTime;
+}

+ 23 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/account/entity/TransferAccountEntity.java

@@ -0,0 +1,23 @@
+package com.payment.platform.module.payment.invoice.account.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.payment.platform.common.base.PaymentEnterpriseBaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("pay_invoice_transfer_account")
+public class TransferAccountEntity extends PaymentEnterpriseBaseEntity {
+
+    private BigDecimal totalAmount;
+    private BigDecimal pendingAmount;
+    private String bankAccountName;
+    private String bankAccountNo;
+    private String bankName;
+    private String bankBranch;
+    private String bankLocation;
+    private String bankCode;
+}

+ 9 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/account/mapper/TransferAccountMapper.java

@@ -0,0 +1,9 @@
+package com.payment.platform.module.payment.invoice.account.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.payment.platform.module.payment.invoice.account.entity.TransferAccountEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface TransferAccountMapper extends BaseMapper<TransferAccountEntity> {
+}

+ 32 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/account/service/TransferAccountService.java

@@ -0,0 +1,32 @@
+package com.payment.platform.module.payment.invoice.account.service;
+
+import com.payment.platform.module.payment.invoice.account.dto.AccountVO;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+
+@Service
+public class TransferAccountService {
+
+    public AccountVO info() {
+        return buildMock();
+    }
+
+    public AccountVO refresh() {
+        return buildMock();
+    }
+
+    private AccountVO buildMock() {
+        AccountVO vo = new AccountVO();
+        vo.setId(1L);
+        vo.setTotalAmount(new BigDecimal("40.00"));
+        vo.setPendingAmount(new BigDecimal("0.00"));
+        vo.setBankAccountName("支付宝支付科技有限公司");
+        vo.setBankAccountNo("2088882400215288826");
+        vo.setBankName("支付机构备付金集中存管账户");
+        vo.setBankBranch("支付宝-备付金账户");
+        vo.setBankLocation("上海市-上海市");
+        vo.setBankCode("991290000015");
+        return vo;
+    }
+}

+ 40 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/company/controller/CompanyController.java

@@ -0,0 +1,40 @@
+package com.payment.platform.module.payment.invoice.company.controller;
+
+import com.payment.platform.common.response.Result;
+import com.payment.platform.core.security.LoginUser;
+import com.payment.platform.module.payment.invoice.company.dto.CompanyConfigUpdateDTO;
+import com.payment.platform.module.payment.invoice.company.dto.CompanyConfigVO;
+import com.payment.platform.module.payment.invoice.company.service.CompanyConfigService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/payment/invoice/company")
+@RequiredArgsConstructor
+public class CompanyController {
+
+    private final CompanyConfigService companyConfigService;
+
+    private LoginUser currentUser() {
+        Authentication a = SecurityContextHolder.getContext().getAuthentication();
+        if (a != null && a.getPrincipal() instanceof LoginUser u) return u;
+        return null;
+    }
+
+    @GetMapping
+    public Result<CompanyConfigVO> getConfig() {
+        return Result.ok(companyConfigService.getConfig());
+    }
+
+    @PutMapping("/info")
+    public Result<CompanyConfigVO> updateInfo(@RequestBody CompanyConfigUpdateDTO dto) {
+        return Result.ok(companyConfigService.updateInfo(dto));
+    }
+
+    @PutMapping("/invoice")
+    public Result<CompanyConfigVO> updateInvoice(@RequestBody CompanyConfigUpdateDTO dto) {
+        return Result.ok(companyConfigService.updateInvoice(dto));
+    }
+}

+ 11 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/company/dto/CompanyConfigUpdateDTO.java

@@ -0,0 +1,11 @@
+package com.payment.platform.module.payment.invoice.company.dto;
+
+import lombok.Data;
+
+@Data
+public class CompanyConfigUpdateDTO {
+    private String contactName;
+    private String contactPhone;
+    private String defaultInvoiceType;
+    private String defaultTaxRate;
+}

+ 36 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/company/dto/CompanyConfigVO.java

@@ -0,0 +1,36 @@
+package com.payment.platform.module.payment.invoice.company.dto;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.OffsetDateTime;
+
+@Data
+public class CompanyConfigVO implements Serializable {
+    private Long id;
+    private String companyName;
+    private String taxNo;
+    private String bankName;
+    private String bankAccount;
+    private String address;
+    private String phone;
+    private String taxRegion;
+    private String issuerName;
+    private String issuerIdCard;
+    private String contactName;
+    private String contactPhone;
+    private BigDecimal monthlyQuota;
+    private BigDecimal availableQuota;
+    private BigDecimal downloadedQuota;
+    private BigDecimal usedQuota;
+    private String taxMethod;
+    private String defaultInvoiceType;
+    private String defaultTaxRate;
+    private Boolean requireAuditBeforePay;
+    private Boolean inviteToSupplier;
+    private Boolean showPayeeName;
+    private Boolean employeeQuotaEnabled;
+    private String status;
+    private OffsetDateTime createdTime;
+    private OffsetDateTime updatedTime;
+}

+ 37 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/company/entity/CompanyConfigEntity.java

@@ -0,0 +1,37 @@
+package com.payment.platform.module.payment.invoice.company.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.payment.platform.common.base.PaymentEnterpriseBaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("pay_invoice_company_config")
+public class CompanyConfigEntity extends PaymentEnterpriseBaseEntity {
+
+    private String companyName;
+    private String taxNo;
+    private String bankName;
+    private String bankAccount;
+    private String address;
+    private String phone;
+    private String taxRegion;
+    private String issuerName;
+    private String issuerIdCard;
+    private String contactName;
+    private String contactPhone;
+    private BigDecimal monthlyQuota;
+    private BigDecimal availableQuota;
+    private BigDecimal downloadedQuota;
+    private BigDecimal usedQuota;
+    private String taxMethod;
+    private String defaultInvoiceType;
+    private String defaultTaxRate;
+    private Boolean requireAuditBeforePay;
+    private Boolean inviteToSupplier;
+    private Boolean showPayeeName;
+    private Boolean employeeQuotaEnabled;
+}

+ 9 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/company/mapper/CompanyConfigMapper.java

@@ -0,0 +1,9 @@
+package com.payment.platform.module.payment.invoice.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.payment.platform.module.payment.invoice.company.entity.CompanyConfigEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface CompanyConfigMapper extends BaseMapper<CompanyConfigEntity> {
+}

+ 105 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/company/service/CompanyConfigService.java

@@ -0,0 +1,105 @@
+package com.payment.platform.module.payment.invoice.company.service;
+
+import com.payment.platform.module.payment.invoice.company.dto.CompanyConfigUpdateDTO;
+import com.payment.platform.module.payment.invoice.company.dto.CompanyConfigVO;
+import com.payment.platform.module.payment.invoice.company.entity.CompanyConfigEntity;
+import jakarta.annotation.PostConstruct;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.OffsetDateTime;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+@Service
+public class CompanyConfigService {
+
+    private final ConcurrentHashMap<Long, CompanyConfigEntity> store = new ConcurrentHashMap<>();
+    private final AtomicLong idGen = new AtomicLong(2000);
+
+    @PostConstruct
+    public void initMockData() {
+        CompanyConfigEntity e = new CompanyConfigEntity();
+        e.setId(idGen.incrementAndGet());
+        e.setCompanyName("湖南省铭恩商务管理有限公司");
+        e.setTaxNo("91430681MACKY5ABXW");
+        e.setBankName("中国光大银行股份有限公司岳阳汨罗支行");
+        e.setBankAccount("53390188000096012");
+        e.setAddress("湖南省岳阳市汨罗市新市镇循环经济产业园鸿昱新路南侧天立路西侧(办公楼)101-201室");
+        e.setPhone("-");
+        e.setTaxRegion("国家税务总局汨罗市税务局第二税务所");
+        e.setIssuerName("童述");
+        e.setIssuerIdCard("4****************4");
+        e.setContactName("姚双");
+        e.setContactPhone("18810729710");
+        e.setMonthlyQuota(new BigDecimal("10000000.00"));
+        e.setAvailableQuota(new BigDecimal("9999990.00"));
+        e.setDownloadedQuota(new BigDecimal("10.00"));
+        e.setUsedQuota(BigDecimal.ZERO);
+        e.setTaxMethod("SIMPLIFIED");
+        e.setDefaultInvoiceType("ORDINARY");
+        e.setDefaultTaxRate("1%");
+        e.setRequireAuditBeforePay(false);
+        e.setInviteToSupplier(false);
+        e.setShowPayeeName(false);
+        e.setEmployeeQuotaEnabled(false);
+        e.setStatus("1");
+        e.setCreatedTime(OffsetDateTime.now());
+        e.setUpdatedTime(OffsetDateTime.now());
+        store.put(e.getId(), e);
+    }
+
+    public CompanyConfigVO getConfig() {
+        CompanyConfigEntity e = store.values().stream().findFirst().orElse(null);
+        return e != null ? toVO(e) : null;
+    }
+
+    public CompanyConfigVO updateInfo(CompanyConfigUpdateDTO dto) {
+        CompanyConfigEntity e = store.values().stream().findFirst().orElse(null);
+        if (e == null) return null;
+        if (dto.getContactName() != null) e.setContactName(dto.getContactName());
+        if (dto.getContactPhone() != null) e.setContactPhone(dto.getContactPhone());
+        e.setUpdatedTime(OffsetDateTime.now());
+        return toVO(e);
+    }
+
+    public CompanyConfigVO updateInvoice(CompanyConfigUpdateDTO dto) {
+        CompanyConfigEntity e = store.values().stream().findFirst().orElse(null);
+        if (e == null) return null;
+        if (dto.getDefaultInvoiceType() != null) e.setDefaultInvoiceType(dto.getDefaultInvoiceType());
+        if (dto.getDefaultTaxRate() != null) e.setDefaultTaxRate(dto.getDefaultTaxRate());
+        e.setUpdatedTime(OffsetDateTime.now());
+        return toVO(e);
+    }
+
+    private CompanyConfigVO toVO(CompanyConfigEntity e) {
+        CompanyConfigVO vo = new CompanyConfigVO();
+        vo.setId(e.getId());
+        vo.setCompanyName(e.getCompanyName());
+        vo.setTaxNo(e.getTaxNo());
+        vo.setBankName(e.getBankName());
+        vo.setBankAccount(e.getBankAccount());
+        vo.setAddress(e.getAddress());
+        vo.setPhone(e.getPhone());
+        vo.setTaxRegion(e.getTaxRegion());
+        vo.setIssuerName(e.getIssuerName());
+        vo.setIssuerIdCard(e.getIssuerIdCard());
+        vo.setContactName(e.getContactName());
+        vo.setContactPhone(e.getContactPhone());
+        vo.setMonthlyQuota(e.getMonthlyQuota());
+        vo.setAvailableQuota(e.getAvailableQuota());
+        vo.setDownloadedQuota(e.getDownloadedQuota());
+        vo.setUsedQuota(e.getUsedQuota());
+        vo.setTaxMethod(e.getTaxMethod());
+        vo.setDefaultInvoiceType(e.getDefaultInvoiceType());
+        vo.setDefaultTaxRate(e.getDefaultTaxRate());
+        vo.setRequireAuditBeforePay(e.getRequireAuditBeforePay());
+        vo.setInviteToSupplier(e.getInviteToSupplier());
+        vo.setShowPayeeName(e.getShowPayeeName());
+        vo.setEmployeeQuotaEnabled(e.getEmployeeQuotaEnabled());
+        vo.setStatus(e.getStatus());
+        vo.setCreatedTime(e.getCreatedTime());
+        vo.setUpdatedTime(e.getUpdatedTime());
+        return vo;
+    }
+}

+ 50 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/employee/controller/EmployeeController.java

@@ -0,0 +1,50 @@
+package com.payment.platform.module.payment.invoice.employee.controller;
+
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.common.response.Result;
+import com.payment.platform.core.security.LoginUser;
+import com.payment.platform.module.payment.invoice.employee.dto.EmployeeCreateDTO;
+import com.payment.platform.module.payment.invoice.employee.dto.EmployeeQueryDTO;
+import com.payment.platform.module.payment.invoice.employee.dto.EmployeeVO;
+import com.payment.platform.module.payment.invoice.employee.service.EmployeeService;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/payment/invoice/employee")
+@RequiredArgsConstructor
+public class EmployeeController {
+
+    private final EmployeeService employeeService;
+
+    private LoginUser currentUser() {
+        Authentication a = SecurityContextHolder.getContext().getAuthentication();
+        if (a != null && a.getPrincipal() instanceof LoginUser u) return u;
+        return null;
+    }
+
+    @GetMapping
+    public Result<PageResult<EmployeeVO>> list(EmployeeQueryDTO query) {
+        return Result.ok(employeeService.list(query));
+    }
+
+    @PostMapping
+    public Result<EmployeeVO> create(@Valid @RequestBody EmployeeCreateDTO body) {
+        return Result.ok(employeeService.create(body));
+    }
+
+    @PutMapping("/{id}")
+    public Result<EmployeeVO> update(@PathVariable Long id, @Valid @RequestBody EmployeeCreateDTO body) {
+        return Result.ok(employeeService.update(id, body));
+    }
+
+    @DeleteMapping("/{id}")
+    public Result<Map<String, Object>> delete(@PathVariable Long id) {
+        return Result.ok(employeeService.delete(id));
+    }
+}

+ 19 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/employee/dto/EmployeeCreateDTO.java

@@ -0,0 +1,19 @@
+package com.payment.platform.module.payment.invoice.employee.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class EmployeeCreateDTO {
+
+    @NotBlank
+    private String name;
+
+    private String phone;
+    private String idCard;
+
+    @NotBlank
+    private String role;
+
+    private Boolean allowSelectSupplier;
+}

+ 12 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/employee/dto/EmployeeQueryDTO.java

@@ -0,0 +1,12 @@
+package com.payment.platform.module.payment.invoice.employee.dto;
+
+import lombok.Data;
+
+@Data
+public class EmployeeQueryDTO {
+
+    private Integer pageNo = 1;
+    private Integer pageSize = 10;
+    private String name;
+    private String role;
+}

+ 21 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/employee/dto/EmployeeVO.java

@@ -0,0 +1,21 @@
+package com.payment.platform.module.payment.invoice.employee.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.OffsetDateTime;
+
+@Data
+public class EmployeeVO implements Serializable {
+
+    private Long id;
+    private String name;
+    private String phone;
+    private String idCard;
+    private String role;
+    private Boolean allowSelectSupplier;
+    private Long sysUserId;
+    private String status;
+    private OffsetDateTime createdTime;
+    private OffsetDateTime updatedTime;
+}

+ 19 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/employee/entity/EmployeeEntity.java

@@ -0,0 +1,19 @@
+package com.payment.platform.module.payment.invoice.employee.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.payment.platform.common.base.PaymentTenantBaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("pay_invoice_employee")
+public class EmployeeEntity extends PaymentTenantBaseEntity {
+
+    private String name;
+    private String phone;
+    private String idCard;
+    private String role;
+    private Boolean allowSelectSupplier;
+    private Long sysUserId;
+}

+ 16 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/employee/enums/EmployeeEnums.java

@@ -0,0 +1,16 @@
+package com.payment.platform.module.payment.invoice.employee.enums;
+
+import lombok.Getter;
+
+public final class EmployeeEnums {
+
+    @Getter
+    public enum Role {
+        SUPER_ADMIN("超级管理员"),
+        CLERK("营业员"),
+        ISSUER("开票员");
+
+        private final String label;
+        Role(String label) { this.label = label; }
+    }
+}

+ 9 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/employee/mapper/EmployeeMapper.java

@@ -0,0 +1,9 @@
+package com.payment.platform.module.payment.invoice.employee.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.payment.platform.module.payment.invoice.employee.entity.EmployeeEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface EmployeeMapper extends BaseMapper<EmployeeEntity> {
+}

+ 120 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/employee/service/EmployeeService.java

@@ -0,0 +1,120 @@
+package com.payment.platform.module.payment.invoice.employee.service;
+
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.module.payment.invoice.employee.dto.EmployeeCreateDTO;
+import com.payment.platform.module.payment.invoice.employee.dto.EmployeeQueryDTO;
+import com.payment.platform.module.payment.invoice.employee.dto.EmployeeVO;
+import com.payment.platform.module.payment.invoice.employee.entity.EmployeeEntity;
+import jakarta.annotation.PostConstruct;
+import org.springframework.stereotype.Service;
+
+import java.time.OffsetDateTime;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+@Service
+public class EmployeeService {
+
+    private final ConcurrentHashMap<Long, EmployeeEntity> store = new ConcurrentHashMap<>();
+    private final AtomicLong idGen = new AtomicLong(3000);
+
+    @PostConstruct
+    public void initMockData() {
+        EmployeeEntity e1 = new EmployeeEntity();
+        e1.setId(idGen.incrementAndGet());
+        e1.setName("姚双");
+        e1.setPhone("1****************0");
+        e1.setRole("CLERK");
+        e1.setAllowSelectSupplier(true);
+        e1.setStatus("1");
+        e1.setCreatedTime(OffsetDateTime.now());
+        e1.setUpdatedTime(OffsetDateTime.now());
+        store.put(e1.getId(), e1);
+
+        EmployeeEntity e2 = new EmployeeEntity();
+        e2.setId(idGen.incrementAndGet());
+        e2.setName("湖南省铭恩商务管理有限公司");
+        e2.setRole("SUPER_ADMIN");
+        e2.setAllowSelectSupplier(false);
+        e2.setStatus("1");
+        e2.setCreatedTime(OffsetDateTime.now());
+        e2.setUpdatedTime(OffsetDateTime.now());
+        store.put(e2.getId(), e2);
+
+        EmployeeEntity e3 = new EmployeeEntity();
+        e3.setId(idGen.incrementAndGet());
+        e3.setName("童述");
+        e3.setIdCard("4****************4");
+        e3.setRole("ISSUER");
+        e3.setAllowSelectSupplier(false);
+        e3.setStatus("1");
+        e3.setCreatedTime(OffsetDateTime.now());
+        e3.setUpdatedTime(OffsetDateTime.now());
+        store.put(e3.getId(), e3);
+    }
+
+    public PageResult<EmployeeVO> list(EmployeeQueryDTO query) {
+        List<EmployeeVO> all = store.values().stream()
+                .filter(e -> query.getName() == null || e.getName().contains(query.getName()))
+                .filter(e -> query.getRole() == null || query.getRole().equals(e.getRole()))
+                .map(this::toVO)
+                .collect(Collectors.toList());
+        int total = all.size();
+        int from = (query.getPageNo() - 1) * query.getPageSize();
+        int to = Math.min(from + query.getPageSize(), total);
+        List<EmployeeVO> page = from < total ? all.subList(from, to) : List.of();
+        return PageResult.of(query.getPageNo(), query.getPageSize(), total, page);
+    }
+
+    public EmployeeVO create(EmployeeCreateDTO dto) {
+        EmployeeEntity e = new EmployeeEntity();
+        e.setId(idGen.incrementAndGet());
+        e.setName(dto.getName());
+        e.setPhone(dto.getPhone());
+        e.setIdCard(dto.getIdCard());
+        e.setRole(dto.getRole());
+        e.setAllowSelectSupplier(dto.getAllowSelectSupplier() != null ? dto.getAllowSelectSupplier() : false);
+        e.setStatus("1");
+        e.setCreatedTime(OffsetDateTime.now());
+        e.setUpdatedTime(OffsetDateTime.now());
+        store.put(e.getId(), e);
+        return toVO(e);
+    }
+
+    public EmployeeVO update(Long id, EmployeeCreateDTO dto) {
+        EmployeeEntity e = store.get(id);
+        if (e == null) return null;
+        if (dto.getName() != null) e.setName(dto.getName());
+        if (dto.getPhone() != null) e.setPhone(dto.getPhone());
+        if (dto.getIdCard() != null) e.setIdCard(dto.getIdCard());
+        if (dto.getAllowSelectSupplier() != null) e.setAllowSelectSupplier(dto.getAllowSelectSupplier());
+        e.setUpdatedTime(OffsetDateTime.now());
+        return toVO(e);
+    }
+
+    public Map<String, Object> delete(Long id) {
+        store.remove(id);
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("deleted", id);
+        return result;
+    }
+
+    private EmployeeVO toVO(EmployeeEntity e) {
+        EmployeeVO vo = new EmployeeVO();
+        vo.setId(e.getId());
+        vo.setName(e.getName());
+        vo.setPhone(e.getPhone());
+        vo.setIdCard(e.getIdCard());
+        vo.setRole(e.getRole());
+        vo.setAllowSelectSupplier(e.getAllowSelectSupplier());
+        vo.setSysUserId(e.getSysUserId());
+        vo.setStatus(e.getStatus());
+        vo.setCreatedTime(e.getCreatedTime());
+        vo.setUpdatedTime(e.getUpdatedTime());
+        return vo;
+    }
+}

+ 57 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/goods/controller/GoodsController.java

@@ -0,0 +1,57 @@
+package com.payment.platform.module.payment.invoice.goods.controller;
+
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.common.response.Result;
+import com.payment.platform.core.security.LoginUser;
+import com.payment.platform.module.payment.invoice.goods.dto.GoodsCategoryVO;
+import com.payment.platform.module.payment.invoice.goods.dto.GoodsCreateDTO;
+import com.payment.platform.module.payment.invoice.goods.dto.GoodsQueryDTO;
+import com.payment.platform.module.payment.invoice.goods.dto.GoodsVO;
+import com.payment.platform.module.payment.invoice.goods.service.GoodsService;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/payment/invoice/goods")
+@RequiredArgsConstructor
+public class GoodsController {
+
+    private final GoodsService goodsService;
+
+    private LoginUser currentUser() {
+        Authentication a = SecurityContextHolder.getContext().getAuthentication();
+        if (a != null && a.getPrincipal() instanceof LoginUser u) return u;
+        return null;
+    }
+
+    @GetMapping("/category")
+    public Result<List<GoodsCategoryVO>> tree() {
+        return Result.ok(goodsService.getCategoryTree());
+    }
+
+    @GetMapping
+    public Result<PageResult<GoodsVO>> list(GoodsQueryDTO query) {
+        return Result.ok(goodsService.list(query));
+    }
+
+    @PostMapping
+    public Result<GoodsVO> create(@Valid @RequestBody GoodsCreateDTO body) {
+        return Result.ok(goodsService.create(body));
+    }
+
+    @PutMapping("/{id}")
+    public Result<GoodsVO> update(@PathVariable Long id, @Valid @RequestBody GoodsCreateDTO body) {
+        return Result.ok(goodsService.update(id, body));
+    }
+
+    @DeleteMapping("/{id}")
+    public Result<Map<String, Object>> delete(@PathVariable Long id) {
+        return Result.ok(goodsService.delete(id));
+    }
+}

+ 17 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/goods/dto/GoodsCategoryVO.java

@@ -0,0 +1,17 @@
+package com.payment.platform.module.payment.invoice.goods.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class GoodsCategoryVO implements Serializable {
+
+    private Long id;
+    private Long parentId;
+    private String name;
+    private Integer sortOrder;
+    private String productCode;
+    private List<GoodsCategoryVO> children;
+}

+ 18 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/goods/dto/GoodsCreateDTO.java

@@ -0,0 +1,18 @@
+package com.payment.platform.module.payment.invoice.goods.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class GoodsCreateDTO {
+
+    @NotNull
+    private Long categoryId;
+
+    @NotBlank
+    private String name;
+
+    private String unit;
+    private String spec;
+}

+ 11 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/goods/dto/GoodsQueryDTO.java

@@ -0,0 +1,11 @@
+package com.payment.platform.module.payment.invoice.goods.dto;
+
+import lombok.Data;
+
+@Data
+public class GoodsQueryDTO {
+
+    private Integer pageNo = 1;
+    private Integer pageSize = 10;
+    private Long categoryId;
+}

+ 20 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/goods/dto/GoodsVO.java

@@ -0,0 +1,20 @@
+package com.payment.platform.module.payment.invoice.goods.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.OffsetDateTime;
+
+@Data
+public class GoodsVO implements Serializable {
+
+    private Long id;
+    private Long categoryId;
+    private String name;
+    private String unit;
+    private String spec;
+    private String enterpriseId;
+    private String status;
+    private OffsetDateTime createdTime;
+    private OffsetDateTime updatedTime;
+}

+ 17 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/goods/entity/GoodsCategoryEntity.java

@@ -0,0 +1,17 @@
+package com.payment.platform.module.payment.invoice.goods.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.payment.platform.common.base.PaymentTenantBaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("pay_invoice_goods_category")
+public class GoodsCategoryEntity extends PaymentTenantBaseEntity {
+
+    private Long parentId;
+    private String name;
+    private Integer sortOrder;
+    private String productCode;
+}

+ 18 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/goods/entity/GoodsEntity.java

@@ -0,0 +1,18 @@
+package com.payment.platform.module.payment.invoice.goods.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.payment.platform.common.base.PaymentTenantBaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("pay_invoice_goods")
+public class GoodsEntity extends PaymentTenantBaseEntity {
+
+    private Long categoryId;
+    private String name;
+    private String unit;
+    private String spec;
+    private String enterpriseId;
+}

+ 9 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/goods/mapper/GoodsCategoryMapper.java

@@ -0,0 +1,9 @@
+package com.payment.platform.module.payment.invoice.goods.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.payment.platform.module.payment.invoice.goods.entity.GoodsCategoryEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface GoodsCategoryMapper extends BaseMapper<GoodsCategoryEntity> {
+}

+ 9 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/goods/mapper/GoodsMapper.java

@@ -0,0 +1,9 @@
+package com.payment.platform.module.payment.invoice.goods.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.payment.platform.module.payment.invoice.goods.entity.GoodsEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface GoodsMapper extends BaseMapper<GoodsEntity> {
+}

+ 165 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/goods/service/GoodsService.java

@@ -0,0 +1,165 @@
+package com.payment.platform.module.payment.invoice.goods.service;
+
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.module.payment.invoice.goods.dto.GoodsCategoryVO;
+import com.payment.platform.module.payment.invoice.goods.dto.GoodsCreateDTO;
+import com.payment.platform.module.payment.invoice.goods.dto.GoodsQueryDTO;
+import com.payment.platform.module.payment.invoice.goods.dto.GoodsVO;
+import com.payment.platform.module.payment.invoice.goods.entity.GoodsCategoryEntity;
+import com.payment.platform.module.payment.invoice.goods.entity.GoodsEntity;
+import jakarta.annotation.PostConstruct;
+import org.springframework.stereotype.Service;
+
+import java.time.OffsetDateTime;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+@Service
+public class GoodsService {
+
+    private final ConcurrentHashMap<Long, GoodsCategoryEntity> catStore = new ConcurrentHashMap<>();
+    private final ConcurrentHashMap<Long, GoodsEntity> goodsStore = new ConcurrentHashMap<>();
+    private final AtomicLong idGen = new AtomicLong(4000);
+
+    @PostConstruct
+    public void initMockData() {
+        // 9 main categories with sub-categories
+        long root = createCategory(0L, "报废产品", 0, "SCRAP_MATERIAL");
+        long feiGangTie = createCategory(root, "废钢铁", 1, null);
+        createCategory(feiGangTie, "制造性废钢铁", 1, null);
+        createCategory(feiGangTie, "农业废钢铁", 2, null);
+        createCategory(feiGangTie, "建筑业废钢铁", 3, null);
+        createCategory(feiGangTie, "家用废钢铁", 4, null);
+        createCategory(feiGangTie, "机器设备废钢铁", 5, null);
+        createCategory(feiGangTie, "其他废钢铁", 6, null);
+        long feiYouSe = createCategory(root, "废有色金属", 2, null);
+        createCategory(feiYouSe, "废铜", 1, null);
+        createCategory(feiYouSe, "废铝", 2, null);
+        createCategory(feiYouSe, "废铅", 3, null);
+        createCategory(feiYouSe, "废锌", 4, null);
+        createCategory(feiYouSe, "废稀贵金属", 5, null);
+        createCategory(feiYouSe, "其他废有色金属", 6, null);
+        createCategory(root, "废塑料", 3, null);
+        createCategory(root, "废轮胎", 4, null);
+        createCategory(root, "废纸", 5, null);
+        long dianQi = createCategory(root, "废弃电器电子产品", 6, null);
+        createCategory(dianQi, "废电视机", 1, null);
+        createCategory(dianQi, "废电冰箱", 2, null);
+        createCategory(dianQi, "废洗衣机", 3, null);
+        createCategory(dianQi, "废空调", 4, null);
+        createCategory(dianQi, "废电脑", 5, null);
+        createCategory(dianQi, "废手机", 6, null);
+        createCategory(dianQi, "其他废弃电器电子产品", 7, null);
+        long jiDongChe = createCategory(root, "报废机动车", 7, null);
+        createCategory(jiDongChe, "报废汽车", 1, null);
+        createCategory(jiDongChe, "报废摩托车", 2, null);
+        createCategory(jiDongChe, "其他报废机动车", 3, null);
+        createCategory(root, "废旧纺织品", 8, null);
+        createCategory(root, "废玻璃", 9, null);
+        long dianChi = createCategory(root, "废电池", 10, null);
+        createCategory(dianChi, "废铅蓄电池", 1, null);
+        createCategory(dianChi, "废锂离子电池", 2, null);
+        createCategory(dianChi, "废镍氢电池", 3, null);
+        createCategory(dianChi, "其他废电池", 4, null);
+        long qiTa = createCategory(root, "其他报废产品", 11, null);
+        createCategory(qiTa, "其他生活类报废产品", 1, null);
+        createCategory(qiTa, "报废船舶", 2, null);
+        createCategory(qiTa, "其他未列明报废产品", 3, null);
+    }
+
+    private long createCategory(Long parentId, String name, int sortOrder, String productCode) {
+        GoodsCategoryEntity e = new GoodsCategoryEntity();
+        e.setId(idGen.incrementAndGet());
+        e.setParentId(parentId);
+        e.setName(name);
+        e.setSortOrder(sortOrder);
+        e.setProductCode(productCode);
+        e.setStatus("1");
+        e.setCreatedTime(OffsetDateTime.now());
+        e.setUpdatedTime(OffsetDateTime.now());
+        catStore.put(e.getId(), e);
+        return e.getId();
+    }
+
+    public List<GoodsCategoryVO> getCategoryTree() {
+        List<GoodsCategoryEntity> all = new ArrayList<>(catStore.values());
+        Map<Long, List<GoodsCategoryEntity>> byParent = all.stream()
+                .collect(Collectors.groupingBy(GoodsCategoryEntity::getParentId));
+        return buildTree(0L, byParent);
+    }
+
+    private List<GoodsCategoryVO> buildTree(Long parentId, Map<Long, List<GoodsCategoryEntity>> byParent) {
+        List<GoodsCategoryEntity> children = byParent.getOrDefault(parentId, List.of());
+        children.sort(Comparator.comparing(GoodsCategoryEntity::getSortOrder));
+        return children.stream().map(e -> {
+            GoodsCategoryVO vo = new GoodsCategoryVO();
+            vo.setId(e.getId());
+            vo.setParentId(e.getParentId());
+            vo.setName(e.getName());
+            vo.setSortOrder(e.getSortOrder());
+            vo.setProductCode(e.getProductCode());
+            vo.setChildren(buildTree(e.getId(), byParent));
+            return vo;
+        }).collect(Collectors.toList());
+    }
+
+    public PageResult<GoodsVO> list(GoodsQueryDTO query) {
+        List<GoodsVO> filtered = goodsStore.values().stream()
+                .filter(g -> query.getCategoryId() == null || query.getCategoryId().equals(g.getCategoryId()))
+                .map(this::toVO)
+                .collect(Collectors.toList());
+        int total = filtered.size();
+        int from = (query.getPageNo() - 1) * query.getPageSize();
+        int to = Math.min(from + query.getPageSize(), total);
+        List<GoodsVO> page = from < total ? filtered.subList(from, to) : List.of();
+        return PageResult.of(query.getPageNo(), query.getPageSize(), total, page);
+    }
+
+    public GoodsVO create(GoodsCreateDTO dto) {
+        GoodsEntity e = new GoodsEntity();
+        e.setId(idGen.incrementAndGet());
+        e.setCategoryId(dto.getCategoryId());
+        e.setName(dto.getName());
+        e.setUnit(dto.getUnit());
+        e.setSpec(dto.getSpec());
+        e.setStatus("1");
+        e.setCreatedTime(OffsetDateTime.now());
+        e.setUpdatedTime(OffsetDateTime.now());
+        goodsStore.put(e.getId(), e);
+        return toVO(e);
+    }
+
+    public GoodsVO update(Long id, GoodsCreateDTO dto) {
+        GoodsEntity e = goodsStore.get(id);
+        if (e == null) return null;
+        if (dto.getCategoryId() != null) e.setCategoryId(dto.getCategoryId());
+        if (dto.getName() != null) e.setName(dto.getName());
+        if (dto.getUnit() != null) e.setUnit(dto.getUnit());
+        if (dto.getSpec() != null) e.setSpec(dto.getSpec());
+        e.setUpdatedTime(OffsetDateTime.now());
+        return toVO(e);
+    }
+
+    public Map<String, Object> delete(Long id) {
+        goodsStore.remove(id);
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("deleted", id);
+        return result;
+    }
+
+    private GoodsVO toVO(GoodsEntity e) {
+        GoodsVO vo = new GoodsVO();
+        vo.setId(e.getId());
+        vo.setCategoryId(e.getCategoryId());
+        vo.setName(e.getName());
+        vo.setUnit(e.getUnit());
+        vo.setSpec(e.getSpec());
+        vo.setEnterpriseId(e.getEnterpriseId());
+        vo.setStatus(e.getStatus());
+        vo.setCreatedTime(e.getCreatedTime());
+        vo.setUpdatedTime(e.getUpdatedTime());
+        return vo;
+    }
+}

+ 64 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/order/controller/OrderController.java

@@ -0,0 +1,64 @@
+package com.payment.platform.module.payment.invoice.order.controller;
+
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.common.response.Result;
+import com.payment.platform.core.security.LoginUser;
+import com.payment.platform.module.payment.invoice.order.dto.*;
+import com.payment.platform.module.payment.invoice.order.service.OrderService;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/payment/invoice/order")
+@RequiredArgsConstructor
+public class OrderController {
+
+    private final OrderService orderService;
+
+    private LoginUser currentUser() {
+        Authentication a = SecurityContextHolder.getContext().getAuthentication();
+        if (a != null && a.getPrincipal() instanceof LoginUser u) return u;
+        return null;
+    }
+
+    @GetMapping
+    public Result<PageResult<OrderVO>> list(OrderQueryDTO query) {
+        return Result.ok(orderService.list(query));
+    }
+
+    @GetMapping("/{id}")
+    public Result<OrderVO> detail(@PathVariable Long id) {
+        return Result.ok(orderService.detail(id));
+    }
+
+    @GetMapping("/{id}/invoice")
+    public Result<List<InvoiceVO>> invoice(@PathVariable Long id) {
+        return Result.ok(orderService.getInvoice(id));
+    }
+
+    @PostMapping("/batch-import")
+    public Result<Map<String, Object>> batchImport(@Valid @RequestBody OrderBatchImportDTO body) {
+        return Result.ok(orderService.batchImport(body));
+    }
+
+    @PostMapping("/batch-cancel")
+    public Result<Map<String, Object>> batchCancel(@RequestBody List<Long> ids) {
+        return Result.ok(orderService.batchCancel(ids));
+    }
+
+    @GetMapping("/export/order")
+    public Result<Map<String, String>> exportOrder() {
+        return Result.ok(orderService.exportOrder());
+    }
+
+    @GetMapping("/export/invoice")
+    public Result<Map<String, String>> exportInvoice() {
+        return Result.ok(orderService.exportInvoice());
+    }
+}

+ 11 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/order/dto/InvoiceVO.java

@@ -0,0 +1,11 @@
+package com.payment.platform.module.payment.invoice.order.dto;
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+@Data
+public class InvoiceVO implements Serializable {
+    private String type;  // RED / BLUE
+    private String invoiceNo;
+    private BigDecimal taxAmount;
+    private String redStatus; // for red invoice only: RED_SUCCESS
+}

+ 6 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/order/dto/OrderBatchImportDTO.java

@@ -0,0 +1,6 @@
+package com.payment.platform.module.payment.invoice.order.dto;
+import lombok.Data;
+@Data
+public class OrderBatchImportDTO {
+    private String fileUrl;
+}

+ 19 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/order/dto/OrderCreateDTO.java

@@ -0,0 +1,19 @@
+package com.payment.platform.module.payment.invoice.order.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class OrderCreateDTO {
+    @NotBlank
+    private String orderNo;
+    private String naturalPersonName;
+    private String naturalPersonPhone;
+    private String collectionAccountType;
+    private String collectionAccount;
+    private BigDecimal taxAmount;
+    private String clerkName;
+    private String productCode;
+}

+ 16 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/order/dto/OrderItemVO.java

@@ -0,0 +1,16 @@
+package com.payment.platform.module.payment.invoice.order.dto;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+public class OrderItemVO implements Serializable {
+    private Long id;
+    private Long orderId;
+    private Integer seqNo;
+    private String goodsName;
+    private BigDecimal unitPrice;
+    private BigDecimal quantity;
+    private BigDecimal amount;
+}

+ 21 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/order/dto/OrderQueryDTO.java

@@ -0,0 +1,21 @@
+package com.payment.platform.module.payment.invoice.order.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+
+@Data
+public class OrderQueryDTO {
+    private Integer pageNo = 1;
+    private Integer pageSize = 10;
+    private String orderTimeStart;
+    private String orderTimeEnd;
+    private String clerkName;
+    private String tradeStatus;
+    private BigDecimal taxAmountMin;
+    private BigDecimal taxAmountMax;
+    private String paymentTimeStart;
+    private String paymentTimeEnd;
+    private String invoiceNo;
+    private String naturalPersonName;
+    private String collectionAccountType;
+}

+ 42 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/order/dto/OrderVO.java

@@ -0,0 +1,42 @@
+package com.payment.platform.module.payment.invoice.order.dto;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.OffsetDateTime;
+import java.util.List;
+
+@Data
+public class OrderVO implements Serializable {
+    private Long id;
+    private String orderNo;
+    private String alipayTradeNo;
+    private OffsetDateTime orderTime;
+    private OffsetDateTime paymentTime;
+    private String naturalPersonName;
+    private String naturalPersonPhone;
+    private String collectionAccountType;
+    private String collectionAccount;
+    private BigDecimal taxAmount;
+    private String tradeStatus;
+    private String invoiceNo;
+    private BigDecimal invoicePreTaxAmount;
+    private BigDecimal invoiceTaxAmount;
+    private String redInvoiceNo;
+    private String clerkName;
+    private String attachments;
+    private String productCode;
+    private BigDecimal orderTotalAmount;
+    private BigDecimal goodsAmount;
+    private BigDecimal totalTaxPaid;
+    private BigDecimal personalIncomeTax;
+    private BigDecimal valueAddedTax;
+    private BigDecimal urbanMaintenanceTax;
+    private BigDecimal educationSurcharge;
+    private BigDecimal localEducationSurcharge;
+    private String status;
+    private OffsetDateTime createdTime;
+    private OffsetDateTime updatedTime;
+    private List<OrderItemVO> items;
+    private List<TaxDetailVO> taxDetails;
+}

+ 12 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/order/dto/TaxDetailVO.java

@@ -0,0 +1,12 @@
+package com.payment.platform.module.payment.invoice.order.dto;
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+@Data
+public class TaxDetailVO implements Serializable {
+    private Long id;
+    private Long orderId;
+    private String taxType;
+    private String taxName;
+    private BigDecimal taxAmount;
+}

+ 41 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/order/entity/OrderEntity.java

@@ -0,0 +1,41 @@
+package com.payment.platform.module.payment.invoice.order.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.payment.platform.common.base.PaymentEnterpriseBaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.time.OffsetDateTime;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("pay_invoice_order")
+public class OrderEntity extends PaymentEnterpriseBaseEntity {
+
+    private String orderNo;
+    private String alipayTradeNo;
+    private OffsetDateTime orderTime;
+    private OffsetDateTime paymentTime;
+    private String naturalPersonName;
+    private String naturalPersonPhone;
+    private String collectionAccountType;
+    private String collectionAccount;
+    private BigDecimal taxAmount;
+    private String tradeStatus;
+    private String invoiceNo;
+    private BigDecimal invoicePreTaxAmount;
+    private BigDecimal invoiceTaxAmount;
+    private String redInvoiceNo;
+    private String clerkName;
+    private String attachments;
+    private String productCode;
+    private BigDecimal orderTotalAmount;
+    private BigDecimal goodsAmount;
+    private BigDecimal totalTaxPaid;
+    private BigDecimal personalIncomeTax;
+    private BigDecimal valueAddedTax;
+    private BigDecimal urbanMaintenanceTax;
+    private BigDecimal educationSurcharge;
+    private BigDecimal localEducationSurcharge;
+}

+ 21 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/order/entity/OrderItemEntity.java

@@ -0,0 +1,21 @@
+package com.payment.platform.module.payment.invoice.order.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.payment.platform.common.base.PaymentEnterpriseBaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("pay_invoice_order_item")
+public class OrderItemEntity extends PaymentEnterpriseBaseEntity {
+
+    private Long orderId;
+    private Integer seqNo;
+    private String goodsName;
+    private BigDecimal unitPrice;
+    private BigDecimal quantity;
+    private BigDecimal amount;
+}

+ 19 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/order/entity/TaxDetailEntity.java

@@ -0,0 +1,19 @@
+package com.payment.platform.module.payment.invoice.order.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.payment.platform.common.base.PaymentEnterpriseBaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("pay_invoice_tax_detail")
+public class TaxDetailEntity extends PaymentEnterpriseBaseEntity {
+
+    private Long orderId;
+    private String taxType;
+    private String taxName;
+    private BigDecimal taxAmount;
+}

+ 30 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/order/enums/OrderEnums.java

@@ -0,0 +1,30 @@
+package com.payment.platform.module.payment.invoice.order.enums;
+
+import lombok.Getter;
+
+public final class OrderEnums {
+
+    @Getter
+    public enum TradeStatus {
+        WAIT_LINK("待关联"),
+        WAIT_AUDIT("待审核"),
+        WAIT_CONFIRM("待确认"),
+        CONFIRMED("已确认"),
+        WAIT_PAY("待支付"),
+        SUCCESS("交易成功"),
+        CANCELLED("订单取消"),
+        FAILED("订单失败");
+
+        private final String label;
+        TradeStatus(String label) { this.label = label; }
+    }
+
+    @Getter
+    public enum AccountType {
+        ALIPAY("支付宝"),
+        BANKCARD("银行卡");
+
+        private final String label;
+        AccountType(String label) { this.label = label; }
+    }
+}

+ 9 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/order/mapper/OrderItemMapper.java

@@ -0,0 +1,9 @@
+package com.payment.platform.module.payment.invoice.order.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.payment.platform.module.payment.invoice.order.entity.OrderItemEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface OrderItemMapper extends BaseMapper<OrderItemEntity> {
+}

+ 9 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/order/mapper/OrderMapper.java

@@ -0,0 +1,9 @@
+package com.payment.platform.module.payment.invoice.order.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.payment.platform.module.payment.invoice.order.entity.OrderEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface OrderMapper extends BaseMapper<OrderEntity> {
+}

+ 9 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/order/mapper/TaxDetailMapper.java

@@ -0,0 +1,9 @@
+package com.payment.platform.module.payment.invoice.order.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.payment.platform.module.payment.invoice.order.entity.TaxDetailEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface TaxDetailMapper extends BaseMapper<TaxDetailEntity> {
+}

+ 219 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/order/service/OrderService.java

@@ -0,0 +1,219 @@
+package com.payment.platform.module.payment.invoice.order.service;
+
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.module.payment.invoice.order.dto.*;
+import com.payment.platform.module.payment.invoice.order.entity.OrderEntity;
+import com.payment.platform.module.payment.invoice.order.entity.OrderItemEntity;
+import com.payment.platform.module.payment.invoice.order.entity.TaxDetailEntity;
+import jakarta.annotation.PostConstruct;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.OffsetDateTime;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+@Service
+public class OrderService {
+
+    private final ConcurrentHashMap<Long, OrderEntity> orderStore = new ConcurrentHashMap<>();
+    private final ConcurrentHashMap<Long, List<OrderItemEntity>> itemStore = new ConcurrentHashMap<>();
+    private final ConcurrentHashMap<Long, List<TaxDetailEntity>> taxStore = new ConcurrentHashMap<>();
+    private final AtomicLong idGen = new AtomicLong(1000);
+
+    @PostConstruct
+    public void initMockData() {
+        long id = idGen.incrementAndGet();
+        OrderEntity order = new OrderEntity();
+        order.setId(id);
+        order.setOrderNo("2026070700152005720055879271");
+        order.setAlipayTradeNo("20260707020070061550010074092577");
+        order.setOrderTime(OffsetDateTime.parse("2026-07-07T15:08:51+08:00"));
+        order.setPaymentTime(OffsetDateTime.parse("2026-07-07T15:12:23+08:00"));
+        order.setNaturalPersonName("刘祥权");
+        order.setNaturalPersonPhone("18684748729");
+        order.setCollectionAccountType("ALIPAY");
+        order.setCollectionAccount("18684748729");
+        order.setTaxAmount(new BigDecimal("10.00"));
+        order.setTradeStatus("SUCCESS");
+        order.setInvoiceNo("26437200000005616855");
+        order.setInvoicePreTaxAmount(new BigDecimal("9.90"));
+        order.setInvoiceTaxAmount(new BigDecimal("0.10"));
+        order.setRedInvoiceNo("26437200000005616856");
+        order.setClerkName("姚双");
+        order.setProductCode("SCRAP_MATERIAL");
+        order.setOrderTotalAmount(new BigDecimal("10.00"));
+        order.setGoodsAmount(new BigDecimal("9.98"));
+        order.setTotalTaxPaid(new BigDecimal("0.02"));
+        order.setPersonalIncomeTax(new BigDecimal("0.02"));
+        order.setValueAddedTax(BigDecimal.ZERO);
+        order.setUrbanMaintenanceTax(BigDecimal.ZERO);
+        order.setEducationSurcharge(BigDecimal.ZERO);
+        order.setLocalEducationSurcharge(BigDecimal.ZERO);
+        order.setStatus("1");
+        order.setCreatedTime(OffsetDateTime.now());
+        order.setUpdatedTime(OffsetDateTime.now());
+        orderStore.put(id, order);
+
+        OrderItemEntity item = new OrderItemEntity();
+        item.setId(idGen.incrementAndGet());
+        item.setOrderId(id);
+        item.setSeqNo(1);
+        item.setGoodsName("废旧电线电缆拆解物");
+        item.setUnitPrice(new BigDecimal("100.00"));
+        item.setQuantity(new BigDecimal("0.1"));
+        item.setAmount(null);
+        itemStore.put(id, List.of(item));
+
+        TaxDetailEntity tax1 = new TaxDetailEntity();
+        tax1.setId(idGen.incrementAndGet());
+        tax1.setOrderId(id);
+        tax1.setTaxType("GOODS");
+        tax1.setTaxName("货款金额");
+        tax1.setTaxAmount(new BigDecimal("9.98"));
+
+        TaxDetailEntity tax2 = new TaxDetailEntity();
+        tax2.setId(idGen.incrementAndGet());
+        tax2.setOrderId(id);
+        tax2.setTaxType("PIT");
+        tax2.setTaxName("个人所得税");
+        tax2.setTaxAmount(new BigDecimal("0.02"));
+
+        TaxDetailEntity tax3 = new TaxDetailEntity();
+        tax3.setId(idGen.incrementAndGet());
+        tax3.setOrderId(id);
+        tax3.setTaxType("VAT");
+        tax3.setTaxName("增值税");
+        tax3.setTaxAmount(BigDecimal.ZERO);
+
+        taxStore.put(id, List.of(tax1, tax2, tax3));
+    }
+
+    public PageResult<OrderVO> list(OrderQueryDTO query) {
+        List<OrderVO> all = orderStore.values().stream()
+                .filter(o -> query.getTradeStatus() == null || query.getTradeStatus().equals(o.getTradeStatus()))
+                .filter(o -> query.getClerkName() == null || query.getClerkName().equals(o.getClerkName()))
+                .filter(o -> query.getNaturalPersonName() == null || query.getNaturalPersonName().equals(o.getNaturalPersonName()))
+                .filter(o -> query.getCollectionAccountType() == null || query.getCollectionAccountType().equals(o.getCollectionAccountType()))
+                .filter(o -> query.getInvoiceNo() == null || query.getInvoiceNo().equals(o.getInvoiceNo()))
+                .map(this::toVO)
+                .collect(Collectors.toList());
+
+        int total = all.size();
+        int from = (query.getPageNo() - 1) * query.getPageSize();
+        int to = Math.min(from + query.getPageSize(), total);
+        List<OrderVO> page = from < total ? all.subList(from, to) : List.of();
+        return PageResult.of(query.getPageNo(), query.getPageSize(), total, page);
+    }
+
+    public OrderVO detail(Long id) {
+        OrderEntity order = orderStore.get(id);
+        if (order == null) return null;
+        OrderVO vo = toVO(order);
+        vo.setItems(itemStore.getOrDefault(id, List.of()).stream().map(i -> {
+            OrderItemVO iv = new OrderItemVO();
+            iv.setId(i.getId());
+            iv.setOrderId(i.getOrderId());
+            iv.setSeqNo(i.getSeqNo());
+            iv.setGoodsName(i.getGoodsName());
+            iv.setUnitPrice(i.getUnitPrice());
+            iv.setQuantity(i.getQuantity());
+            iv.setAmount(i.getAmount());
+            return iv;
+        }).collect(Collectors.toList()));
+        vo.setTaxDetails(taxStore.getOrDefault(id, List.of()).stream().map(t -> {
+            TaxDetailVO tv = new TaxDetailVO();
+            tv.setId(t.getId());
+            tv.setOrderId(t.getOrderId());
+            tv.setTaxType(t.getTaxType());
+            tv.setTaxName(t.getTaxName());
+            tv.setTaxAmount(t.getTaxAmount());
+            return tv;
+        }).collect(Collectors.toList()));
+        return vo;
+    }
+
+    public List<InvoiceVO> getInvoice(Long id) {
+        OrderEntity order = orderStore.get(id);
+        if (order == null) return List.of();
+        InvoiceVO blue = new InvoiceVO();
+        blue.setType("BLUE");
+        blue.setInvoiceNo(order.getInvoiceNo());
+        blue.setTaxAmount(order.getOrderTotalAmount());
+
+        InvoiceVO red = new InvoiceVO();
+        red.setType("RED");
+        red.setInvoiceNo(order.getRedInvoiceNo());
+        red.setTaxAmount(order.getOrderTotalAmount());
+        red.setRedStatus("RED_SUCCESS");
+        return List.of(red, blue);
+    }
+
+    public Map<String, Object> batchImport(OrderBatchImportDTO body) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("taskId", idGen.incrementAndGet());
+        result.put("status", "RUNNING");
+        result.put("message", "导入任务已创建");
+        return result;
+    }
+
+    public Map<String, Object> batchCancel(List<Long> ids) {
+        ids.forEach(id -> {
+            OrderEntity order = orderStore.get(id);
+            if (order != null) order.setTradeStatus("CANCELLED");
+        });
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("cancelled", ids.size());
+        return result;
+    }
+
+    public Map<String, String> exportOrder() {
+        Map<String, String> result = new LinkedHashMap<>();
+        result.put("fileUrl", "/mock/export/order_export_20260708.xlsx");
+        result.put("fileName", "订单导出_20260708.xlsx");
+        return result;
+    }
+
+    public Map<String, String> exportInvoice() {
+        Map<String, String> result = new LinkedHashMap<>();
+        result.put("fileUrl", "/mock/export/invoice_export_20260708.xlsx");
+        result.put("fileName", "发票导出_20260708.xlsx");
+        return result;
+    }
+
+    private OrderVO toVO(OrderEntity e) {
+        OrderVO vo = new OrderVO();
+        vo.setId(e.getId());
+        vo.setOrderNo(e.getOrderNo());
+        vo.setAlipayTradeNo(e.getAlipayTradeNo());
+        vo.setOrderTime(e.getOrderTime());
+        vo.setPaymentTime(e.getPaymentTime());
+        vo.setNaturalPersonName(e.getNaturalPersonName());
+        vo.setNaturalPersonPhone(e.getNaturalPersonPhone());
+        vo.setCollectionAccountType(e.getCollectionAccountType());
+        vo.setCollectionAccount(e.getCollectionAccount());
+        vo.setTaxAmount(e.getTaxAmount());
+        vo.setTradeStatus(e.getTradeStatus());
+        vo.setInvoiceNo(e.getInvoiceNo());
+        vo.setInvoicePreTaxAmount(e.getInvoicePreTaxAmount());
+        vo.setInvoiceTaxAmount(e.getInvoiceTaxAmount());
+        vo.setRedInvoiceNo(e.getRedInvoiceNo());
+        vo.setClerkName(e.getClerkName());
+        vo.setAttachments(e.getAttachments());
+        vo.setProductCode(e.getProductCode());
+        vo.setOrderTotalAmount(e.getOrderTotalAmount());
+        vo.setGoodsAmount(e.getGoodsAmount());
+        vo.setTotalTaxPaid(e.getTotalTaxPaid());
+        vo.setPersonalIncomeTax(e.getPersonalIncomeTax());
+        vo.setValueAddedTax(e.getValueAddedTax());
+        vo.setUrbanMaintenanceTax(e.getUrbanMaintenanceTax());
+        vo.setEducationSurcharge(e.getEducationSurcharge());
+        vo.setLocalEducationSurcharge(e.getLocalEducationSurcharge());
+        vo.setStatus(e.getStatus());
+        vo.setCreatedTime(e.getCreatedTime());
+        vo.setUpdatedTime(e.getUpdatedTime());
+        return vo;
+    }
+}

+ 55 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/supplier/controller/SupplierController.java

@@ -0,0 +1,55 @@
+package com.payment.platform.module.payment.invoice.supplier.controller;
+
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.common.response.Result;
+import com.payment.platform.core.security.LoginUser;
+import com.payment.platform.module.payment.invoice.supplier.dto.SupplierCreateDTO;
+import com.payment.platform.module.payment.invoice.supplier.dto.SupplierQueryDTO;
+import com.payment.platform.module.payment.invoice.supplier.dto.SupplierVO;
+import com.payment.platform.module.payment.invoice.supplier.service.SupplierService;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/payment/invoice/supplier")
+@RequiredArgsConstructor
+public class SupplierController {
+
+    private final SupplierService supplierService;
+
+    private LoginUser currentUser() {
+        Authentication a = SecurityContextHolder.getContext().getAuthentication();
+        if (a != null && a.getPrincipal() instanceof LoginUser u) return u;
+        return null;
+    }
+
+    @GetMapping
+    public Result<PageResult<SupplierVO>> list(SupplierQueryDTO query) {
+        return Result.ok(supplierService.list(query));
+    }
+
+    @PostMapping
+    public Result<SupplierVO> create(@Valid @RequestBody SupplierCreateDTO dto) {
+        return Result.ok(supplierService.create(dto));
+    }
+
+    @PutMapping("/{id}")
+    public Result<SupplierVO> update(@PathVariable Long id, @Valid @RequestBody SupplierCreateDTO dto) {
+        return Result.ok(supplierService.update(id, dto));
+    }
+
+    @DeleteMapping("/{id}")
+    public Result<Map<String, Object>> delete(@PathVariable Long id) {
+        return Result.ok(supplierService.delete(id));
+    }
+
+    @PostMapping("/batch-import")
+    public Result<Map<String, Object>> batchImport(@RequestBody Map<String, Object> body) {
+        return Result.ok(supplierService.batchImport(body));
+    }
+}

+ 19 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/supplier/dto/SupplierCreateDTO.java

@@ -0,0 +1,19 @@
+package com.payment.platform.module.payment.invoice.supplier.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class SupplierCreateDTO {
+
+    @NotBlank
+    private String name;
+
+    @NotBlank
+    private String accountType;
+
+    @NotBlank
+    private String accountNo;
+
+    private String phone;
+}

+ 14 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/supplier/dto/SupplierQueryDTO.java

@@ -0,0 +1,14 @@
+package com.payment.platform.module.payment.invoice.supplier.dto;
+
+import lombok.Data;
+
+@Data
+public class SupplierQueryDTO {
+
+    private Integer pageNo = 1;
+    private Integer pageSize = 10;
+    private String name;
+    private String accountNo;
+    private String phone;
+    private String confirmStatus;
+}

+ 20 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/supplier/dto/SupplierVO.java

@@ -0,0 +1,20 @@
+package com.payment.platform.module.payment.invoice.supplier.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.OffsetDateTime;
+
+@Data
+public class SupplierVO implements Serializable {
+
+    private Long id;
+    private String name;
+    private String accountType;
+    private String accountNo;
+    private String phone;
+    private String confirmStatus;
+    private String status;
+    private OffsetDateTime createdTime;
+    private OffsetDateTime updatedTime;
+}

+ 18 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/supplier/entity/SupplierEntity.java

@@ -0,0 +1,18 @@
+package com.payment.platform.module.payment.invoice.supplier.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.payment.platform.common.base.PaymentEnterpriseBaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("pay_invoice_supplier")
+public class SupplierEntity extends PaymentEnterpriseBaseEntity {
+
+    private String name;
+    private String accountType;
+    private String accountNo;
+    private String phone;
+    private String confirmStatus;
+}

+ 24 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/supplier/enums/SupplierEnums.java

@@ -0,0 +1,24 @@
+package com.payment.platform.module.payment.invoice.supplier.enums;
+
+import lombok.Getter;
+
+public final class SupplierEnums {
+
+    @Getter
+    public enum AccountType {
+        PHONE("支付宝手机号"),
+        EMAIL("支付宝邮箱");
+
+        private final String label;
+        AccountType(String label) { this.label = label; }
+    }
+
+    @Getter
+    public enum ConfirmStatus {
+        PENDING("待确认"),
+        CONFIRMED("已确认");
+
+        private final String label;
+        ConfirmStatus(String label) { this.label = label; }
+    }
+}

+ 9 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/supplier/mapper/SupplierMapper.java

@@ -0,0 +1,9 @@
+package com.payment.platform.module.payment.invoice.supplier.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.payment.platform.module.payment.invoice.supplier.entity.SupplierEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SupplierMapper extends BaseMapper<SupplierEntity> {
+}

+ 122 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/supplier/service/SupplierService.java

@@ -0,0 +1,122 @@
+package com.payment.platform.module.payment.invoice.supplier.service;
+
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.module.payment.invoice.supplier.dto.SupplierCreateDTO;
+import com.payment.platform.module.payment.invoice.supplier.dto.SupplierQueryDTO;
+import com.payment.platform.module.payment.invoice.supplier.dto.SupplierVO;
+import com.payment.platform.module.payment.invoice.supplier.entity.SupplierEntity;
+import com.payment.platform.module.payment.invoice.supplier.enums.SupplierEnums;
+import jakarta.annotation.PostConstruct;
+import org.springframework.stereotype.Service;
+
+import java.time.OffsetDateTime;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+@Service
+public class SupplierService {
+
+    private final ConcurrentHashMap<Long, SupplierEntity> store = new ConcurrentHashMap<>();
+    private final AtomicLong idGen = new AtomicLong(100);
+
+    @PostConstruct
+    public void initMockData() {
+        SupplierEntity s1 = new SupplierEntity();
+        s1.setId(idGen.incrementAndGet());
+        s1.setName("刘祥权");
+        s1.setAccountType(SupplierEnums.AccountType.PHONE.name());
+        s1.setAccountNo("18684748729");
+        s1.setPhone("18684748729");
+        s1.setConfirmStatus(SupplierEnums.ConfirmStatus.CONFIRMED.name());
+        s1.setStatus("1");
+        s1.setCreatedTime(OffsetDateTime.now());
+        s1.setUpdatedTime(OffsetDateTime.now());
+        store.put(s1.getId(), s1);
+
+        SupplierEntity s2 = new SupplierEntity();
+        s2.setId(idGen.incrementAndGet());
+        s2.setName("李四");
+        s2.setAccountType(SupplierEnums.AccountType.EMAIL.name());
+        s2.setAccountNo("li@test.com");
+        s2.setPhone(null);
+        s2.setConfirmStatus(SupplierEnums.ConfirmStatus.PENDING.name());
+        s2.setStatus("1");
+        s2.setCreatedTime(OffsetDateTime.now());
+        s2.setUpdatedTime(OffsetDateTime.now());
+        store.put(s2.getId(), s2);
+    }
+
+    public PageResult<SupplierVO> list(SupplierQueryDTO query) {
+        List<SupplierVO> all = store.values().stream()
+                .filter(e -> query.getName() == null || (e.getName() != null && e.getName().contains(query.getName())))
+                .filter(e -> query.getAccountNo() == null || query.getAccountNo().equals(e.getAccountNo()))
+                .filter(e -> query.getPhone() == null || query.getPhone().equals(e.getPhone()))
+                .filter(e -> query.getConfirmStatus() == null || query.getConfirmStatus().equals(e.getConfirmStatus()))
+                .map(this::toVO)
+                .collect(Collectors.toList());
+
+        int total = all.size();
+        int from = (query.getPageNo() - 1) * query.getPageSize();
+        int to = Math.min(from + query.getPageSize(), total);
+        List<SupplierVO> page = from < total ? all.subList(from, to) : List.of();
+
+        return PageResult.of(query.getPageNo(), query.getPageSize(), total, page);
+    }
+
+    public SupplierVO create(SupplierCreateDTO dto) {
+        SupplierEntity entity = new SupplierEntity();
+        entity.setId(idGen.incrementAndGet());
+        entity.setName(dto.getName());
+        entity.setAccountType(dto.getAccountType());
+        entity.setAccountNo(dto.getAccountNo());
+        entity.setPhone(dto.getPhone());
+        entity.setConfirmStatus(SupplierEnums.ConfirmStatus.PENDING.name());
+        entity.setStatus("1");
+        entity.setCreatedTime(OffsetDateTime.now());
+        entity.setUpdatedTime(OffsetDateTime.now());
+        store.put(entity.getId(), entity);
+        return toVO(entity);
+    }
+
+    public SupplierVO update(Long id, SupplierCreateDTO dto) {
+        SupplierEntity entity = store.get(id);
+        if (entity == null) return null;
+        entity.setName(dto.getName());
+        entity.setAccountType(dto.getAccountType());
+        entity.setAccountNo(dto.getAccountNo());
+        entity.setPhone(dto.getPhone());
+        entity.setUpdatedTime(OffsetDateTime.now());
+        return toVO(entity);
+    }
+
+    public Map<String, Object> delete(Long id) {
+        store.remove(id);
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("success", true);
+        return result;
+    }
+
+    public Map<String, Object> batchImport(Map<String, Object> body) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("taskId", idGen.incrementAndGet());
+        result.put("status", "RUNNING");
+        result.put("message", "导入任务已创建");
+        return result;
+    }
+
+    private SupplierVO toVO(SupplierEntity e) {
+        SupplierVO vo = new SupplierVO();
+        vo.setId(e.getId());
+        vo.setName(e.getName());
+        vo.setAccountType(e.getAccountType());
+        vo.setAccountNo(e.getAccountNo());
+        vo.setPhone(e.getPhone());
+        vo.setConfirmStatus(e.getConfirmStatus());
+        vo.setStatus(e.getStatus());
+        vo.setCreatedTime(e.getCreatedTime());
+        vo.setUpdatedTime(e.getUpdatedTime());
+        return vo;
+    }
+}

+ 31 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/task/controller/TaskController.java

@@ -0,0 +1,31 @@
+package com.payment.platform.module.payment.invoice.task.controller;
+
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.common.response.Result;
+import com.payment.platform.core.security.LoginUser;
+import com.payment.platform.module.payment.invoice.task.dto.TaskQueryDTO;
+import com.payment.platform.module.payment.invoice.task.dto.TaskVO;
+import com.payment.platform.module.payment.invoice.task.service.TaskService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/payment/invoice/task")
+@RequiredArgsConstructor
+public class TaskController {
+
+    private final TaskService taskService;
+
+    private LoginUser currentUser() {
+        Authentication a = SecurityContextHolder.getContext().getAuthentication();
+        if (a != null && a.getPrincipal() instanceof LoginUser u) return u;
+        return null;
+    }
+
+    @GetMapping
+    public Result<PageResult<TaskVO>> list(TaskQueryDTO query) {
+        return Result.ok(taskService.list(query));
+    }
+}

+ 12 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/task/dto/TaskQueryDTO.java

@@ -0,0 +1,12 @@
+package com.payment.platform.module.payment.invoice.task.dto;
+
+import lombok.Data;
+
+@Data
+public class TaskQueryDTO {
+    private Integer pageNo = 1;
+    private Integer pageSize = 10;
+    private String taskType;
+    private String startTimeStart;
+    private String startTimeEnd;
+}

+ 24 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/task/dto/TaskVO.java

@@ -0,0 +1,24 @@
+package com.payment.platform.module.payment.invoice.task.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.OffsetDateTime;
+
+@Data
+public class TaskVO implements Serializable {
+    private Long id;
+    private OffsetDateTime startTime;
+    private OffsetDateTime finishTime;
+    private String product;
+    private String taskType;
+    private String taskStatus;
+    private String fileUrl;
+    private Integer totalCount;
+    private Integer successCount;
+    private Integer failCount;
+    private String errorMsg;
+    private String status;
+    private OffsetDateTime createdTime;
+    private OffsetDateTime updatedTime;
+}

+ 25 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/task/entity/TaskEntity.java

@@ -0,0 +1,25 @@
+package com.payment.platform.module.payment.invoice.task.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.payment.platform.common.base.PaymentEnterpriseBaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.OffsetDateTime;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("pay_invoice_task")
+public class TaskEntity extends PaymentEnterpriseBaseEntity {
+
+    private OffsetDateTime startTime;
+    private OffsetDateTime finishTime;
+    private String product;
+    private String taskType;
+    private String taskStatus;
+    private String fileUrl;
+    private Integer totalCount;
+    private Integer successCount;
+    private Integer failCount;
+    private String errorMsg;
+}

+ 30 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/task/enums/TaskEnums.java

@@ -0,0 +1,30 @@
+package com.payment.platform.module.payment.invoice.task.enums;
+
+import lombok.Getter;
+
+public final class TaskEnums {
+
+    @Getter
+    public enum TaskType {
+        ORDER_IMPORT("订单导入"),
+        SUPPLIER_IMPORT("供应商导入"),
+        TRADE_INVOICE_EXPORT("交易及发票导出"),
+        TRADE_EXPORT("交易导出"),
+        INVOICE_EXPORT("发票导出"),
+        MATERIAL_EXPORT("佐证材料导出"),
+        TAX_RECORD_EXPORT("缴税记录导出");
+
+        private final String label;
+        TaskType(String label) { this.label = label; }
+    }
+
+    @Getter
+    public enum TaskStatus {
+        RUNNING("进行中"),
+        COMPLETED("已完成"),
+        FAILED("失败");
+
+        private final String label;
+        TaskStatus(String label) { this.label = label; }
+    }
+}

+ 9 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/task/mapper/TaskMapper.java

@@ -0,0 +1,9 @@
+package com.payment.platform.module.payment.invoice.task.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.payment.platform.module.payment.invoice.task.entity.TaskEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface TaskMapper extends BaseMapper<TaskEntity> {
+}

+ 30 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/task/service/TaskService.java

@@ -0,0 +1,30 @@
+package com.payment.platform.module.payment.invoice.task.service;
+
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.module.payment.invoice.task.dto.TaskQueryDTO;
+import com.payment.platform.module.payment.invoice.task.dto.TaskVO;
+import com.payment.platform.module.payment.invoice.task.entity.TaskEntity;
+import jakarta.annotation.PostConstruct;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+@Service
+public class TaskService {
+
+    private final ConcurrentHashMap<Long, TaskEntity> store = new ConcurrentHashMap<>();
+    private final AtomicLong idGen = new AtomicLong(3000);
+
+    @PostConstruct
+    public void initMockData() {
+        // empty store initialized
+    }
+
+    public PageResult<TaskVO> list(TaskQueryDTO query) {
+        int pageNo = query.getPageNo();
+        int pageSize = query.getPageSize();
+        return PageResult.of(pageNo, pageSize, 0, new ArrayList<>());
+    }
+}

+ 36 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/tax/controller/TaxController.java

@@ -0,0 +1,36 @@
+package com.payment.platform.module.payment.invoice.tax.controller;
+
+import com.payment.platform.common.response.Result;
+import com.payment.platform.core.security.LoginUser;
+import com.payment.platform.module.payment.invoice.tax.dto.TaxConfigVO;
+import com.payment.platform.module.payment.invoice.tax.service.TaxService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/payment/invoice/tax")
+@RequiredArgsConstructor
+public class TaxController {
+
+    private final TaxService taxService;
+
+    private LoginUser currentUser() {
+        Authentication a = SecurityContextHolder.getContext().getAuthentication();
+        if (a != null && a.getPrincipal() instanceof LoginUser u) return u;
+        return null;
+    }
+
+    @GetMapping("/config")
+    public Result<TaxConfigVO> getConfig() {
+        return Result.ok(taxService.getConfig());
+    }
+
+    @PutMapping("/config")
+    public Result<TaxConfigVO> updateConfig(@RequestBody Map<String, String> body) {
+        return Result.ok(taxService.updateConfig(body));
+    }
+}

+ 13 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/tax/dto/TaxConfigVO.java

@@ -0,0 +1,13 @@
+package com.payment.platform.module.payment.invoice.tax.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class TaxConfigVO implements Serializable {
+    private String taxMode;
+    private String taxModeLabel;
+    private boolean enterpriseEnabled;
+    private String description;
+}

+ 34 - 0
java/src/main/java/com/payment/platform/module/payment/invoice/tax/service/TaxService.java

@@ -0,0 +1,34 @@
+package com.payment.platform.module.payment.invoice.tax.service;
+
+import com.payment.platform.module.payment.invoice.tax.dto.TaxConfigVO;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+
+@Service
+public class TaxService {
+
+    private String currentMode = "PERSONAL";
+
+    public TaxConfigVO getConfig() {
+        TaxConfigVO vo = new TaxConfigVO();
+        vo.setTaxMode(currentMode);
+        if ("PERSONAL".equals(currentMode)) {
+            vo.setTaxModeLabel("个人缴纳");
+            vo.setEnterpriseEnabled(false);
+            vo.setDescription("选择个人缴纳时,由自然人在收款时自行缴纳订单产生的增值税/附加税/个税等税费。");
+        } else {
+            vo.setTaxModeLabel("企业代缴");
+            vo.setEnterpriseEnabled(true);
+            vo.setDescription("选择企业代缴时,由企业统一代扣代缴订单产生的增值税/附加税/个税等税费。");
+        }
+        return vo;
+    }
+
+    public TaxConfigVO updateConfig(Map<String, String> body) {
+        if (body != null && body.containsKey("taxMode")) {
+            this.currentMode = body.get("taxMode");
+        }
+        return getConfig();
+    }
+}