소스 검색

feat: 门店档案模块 - 完整CRUD + 阿里云OSS前端直传

- 新增3张表: t_shop_archive(门店档案主表), t_shop_fixed_asset(固定资产), t_shop_contract_attachment(合同附件)
- 新增12个枚举类: 场地类型/经营状态/合同状态/保证金状态/计费模式/计费方式/资产类型/资产状态/附件类型/打款方式/发票类型/提成基数
- 新增3个Entity + 5个DTO + 3个Mapper + 1个XML映射
- 新增OSS STS前端直传方案: OssConfig + OssService + OssUploadController
- 新增ShopArchiveService/ServiceImpl + ShopArchiveController (7个API)
- 新增前端页面: 列表页 + 5Tab详情页(含字段联动) + OSS上传工具类 + API层
- 新增路由配置 + 菜单权限数据SQL
- 修复OssServiceImpl中AssumeRoleRequest编译错误
skyline 4 일 전
부모
커밋
cd58bc6a7b
44개의 변경된 파일6925개의 추가작업 그리고 2개의 파일을 삭제
  1. 138 0
      docs/database/create_shop_archive.sql
  2. 1928 0
      docs/门店档案模块.md
  3. 2 2
      haha-admin-web/package.json
  4. 369 0
      haha-admin-web/pnpm-lock.yaml
  5. 83 0
      haha-admin-web/src/api/shopArchive.ts
  6. 20 0
      haha-admin-web/src/router/modules/operation.ts
  7. 104 0
      haha-admin-web/src/utils/ossUpload.ts
  8. 1244 0
      haha-admin-web/src/views/shop-archive/detail.vue
  9. 212 0
      haha-admin-web/src/views/shop-archive/index.vue
  10. 245 0
      haha-admin-web/src/views/shop-archive/utils/hook.tsx
  11. 161 0
      haha-admin-web/src/views/shop-archive/utils/types.ts
  12. 49 0
      haha-admin/src/main/java/com/haha/admin/controller/OssUploadController.java
  13. 127 0
      haha-admin/src/main/java/com/haha/admin/controller/ShopArchiveController.java
  14. 13 0
      haha-admin/src/main/resources/application.yml
  15. 31 0
      haha-common/src/main/java/com/haha/common/config/OssConfig.java
  16. 41 0
      haha-common/src/main/java/com/haha/common/enums/AssetStatus.java
  17. 40 0
      haha-common/src/main/java/com/haha/common/enums/AssetType.java
  18. 43 0
      haha-common/src/main/java/com/haha/common/enums/AttachmentType.java
  19. 43 0
      haha-common/src/main/java/com/haha/common/enums/BillingMode.java
  20. 41 0
      haha-common/src/main/java/com/haha/common/enums/BusinessStatus.java
  21. 41 0
      haha-common/src/main/java/com/haha/common/enums/CommissionBaseType.java
  22. 42 0
      haha-common/src/main/java/com/haha/common/enums/ContractStatus.java
  23. 41 0
      haha-common/src/main/java/com/haha/common/enums/DepositStatus.java
  24. 40 0
      haha-common/src/main/java/com/haha/common/enums/InvoiceType.java
  25. 42 0
      haha-common/src/main/java/com/haha/common/enums/PaymentCycle.java
  26. 40 0
      haha-common/src/main/java/com/haha/common/enums/PaymentMethod.java
  27. 40 0
      haha-common/src/main/java/com/haha/common/enums/VenueType.java
  28. 318 0
      haha-entity/src/main/java/com/haha/entity/ShopArchive.java
  29. 95 0
      haha-entity/src/main/java/com/haha/entity/ShopContractAttachment.java
  30. 113 0
      haha-entity/src/main/java/com/haha/entity/ShopFixedAsset.java
  31. 25 0
      haha-entity/src/main/java/com/haha/entity/dto/AttachmentDTO.java
  32. 21 0
      haha-entity/src/main/java/com/haha/entity/dto/FixedAssetDTO.java
  33. 108 0
      haha-entity/src/main/java/com/haha/entity/dto/ShopArchiveCreateDTO.java
  34. 24 0
      haha-entity/src/main/java/com/haha/entity/dto/ShopArchiveQueryDTO.java
  35. 111 0
      haha-entity/src/main/java/com/haha/entity/dto/ShopArchiveUpdateDTO.java
  36. 21 0
      haha-mapper/src/main/java/com/haha/mapper/ShopArchiveMapper.java
  37. 12 0
      haha-mapper/src/main/java/com/haha/mapper/ShopContractAttachmentMapper.java
  38. 12 0
      haha-mapper/src/main/java/com/haha/mapper/ShopFixedAssetMapper.java
  39. 66 0
      haha-mapper/src/main/resources/mapper/ShopArchiveMapper.xml
  40. 18 0
      haha-service/pom.xml
  41. 25 0
      haha-service/src/main/java/com/haha/service/OssService.java
  42. 72 0
      haha-service/src/main/java/com/haha/service/ShopArchiveService.java
  43. 123 0
      haha-service/src/main/java/com/haha/service/impl/OssServiceImpl.java
  44. 541 0
      haha-service/src/main/java/com/haha/service/impl/ShopArchiveServiceImpl.java

+ 138 - 0
docs/database/create_shop_archive.sql

@@ -0,0 +1,138 @@
+-- =====================================================
+-- 门店档案模块建表 + 菜单权限初始化
+-- 日期:2026-07-09
+-- 说明:新建门店档案主表、固定资产设备表、合同附件表,
+--       并在运营管理下新增"门店档案管理"菜单及权限按钮
+-- =====================================================
+
+-- =====================================================
+-- 1. 门店档案主表
+-- =====================================================
+CREATE TABLE IF NOT EXISTS `t_shop_archive` (
+    `id` BIGINT NOT NULL COMMENT '主键ID(雪花算法)',
+    `shop_id` BIGINT NOT NULL COMMENT '关联门店ID(t_shop.id)',
+    `shop_name` VARCHAR(100) NOT NULL COMMENT '门店名称(冗余,便于查询)',
+    `shop_address` VARCHAR(255) DEFAULT NULL COMMENT '门店地址(完整详细地址)',
+    `venue_type` VARCHAR(20) DEFAULT NULL COMMENT '合作场地类型:CHARGING_STATION-充电站, OFFICE_BUILDING-写字楼',
+    `cooperation_mode` VARCHAR(100) DEFAULT NULL COMMENT '合作模式(多选逗号分隔):DRINKS-水饮副食, BOX_MEAL-盒饭',
+    `party_b_salesman` VARCHAR(50) DEFAULT NULL COMMENT '乙方业务员姓名',
+    `party_b_salesman_phone` VARCHAR(20) DEFAULT NULL COMMENT '乙方业务员手机号',
+    `party_a_company` VARCHAR(200) DEFAULT NULL COMMENT '甲方公司名称(签约主体全称)',
+    `party_a_manager` VARCHAR(50) DEFAULT NULL COMMENT '甲方场地管理人员姓名',
+    `party_a_manager_phone` VARCHAR(20) DEFAULT NULL COMMENT '甲方场地管理人员电话',
+    `business_status` VARCHAR(20) DEFAULT 'NORMAL' COMMENT '门店经营状态:NORMAL-正常营业, SUSPENDED-暂停营业, WITHDRAWN-已撤场',
+    `remark` VARCHAR(500) DEFAULT NULL COMMENT '门店备注(特殊要求等)',
+    `contract_no` VARCHAR(100) DEFAULT NULL COMMENT '合同编号(企业归档唯一编码)',
+    `contract_sign_date` DATE DEFAULT NULL COMMENT '合同签订日期',
+    `contract_start_date` DATE DEFAULT NULL COMMENT '合同起始日',
+    `contract_end_date` DATE DEFAULT NULL COMMENT '合同到期日',
+    `contract_status` VARCHAR(20) DEFAULT 'PENDING' COMMENT '合同状态:PENDING-待签约, ACTIVE-已生效, EXPIRING-到期待续约, TERMINATED-已解约终止',
+    `rent_start_date` DATE DEFAULT NULL COMMENT '租金起始日',
+    `monthly_rent` DECIMAL(10,2) DEFAULT NULL COMMENT '每月固定租金金额(元)',
+    `rent_free_days` INT DEFAULT 0 COMMENT '免租期天数',
+    `rent_pay_deadline_day` INT DEFAULT NULL COMMENT '租金缴纳截止日(每月几号)',
+    `deposit_amount` DECIMAL(10,2) DEFAULT NULL COMMENT '保证金(合同约定总额,元)',
+    `deposit_status` VARCHAR(20) DEFAULT 'UNPAID' COMMENT '保证金缴纳状态:UNPAID-未缴纳, PARTIAL-部分缴纳, FULL-全额缴纳',
+    `deposit_paid_amount` DECIMAL(10,2) DEFAULT NULL COMMENT '保证金实缴金额(元)',
+    `deposit_refund_condition` VARCHAR(500) DEFAULT NULL COMMENT '保证金退还条件',
+    `billing_mode` VARCHAR(20) DEFAULT 'RENT_ONLY' COMMENT '合作计费模式:RENT_ONLY-纯租金, RENT_ELECTRIC-租金+电费, COMMISSION_ONLY-纯提成, RENT_COMMISSION-租金+提成, RENT_COMMISSION_ELECTRIC-租金+提成+电费',
+    `electricity_price` DECIMAL(8,4) DEFAULT NULL COMMENT '电费结算单价(元/度)',
+    `electricity_share_rule` VARCHAR(500) DEFAULT NULL COMMENT '电费公摊规则说明',
+    `commission_base_type` VARCHAR(20) DEFAULT NULL COMMENT '提成基数计算口径:SALES-门店实收销售额, GROSS_PROFIT-门店毛利总额, NET_REVENUE-扣除成本后营收',
+    `commission_base` DECIMAL(12,2) DEFAULT NULL COMMENT '提成基数(元)',
+    `commission_rate` DECIMAL(5,2) DEFAULT NULL COMMENT '提成比例(0~100,单位%)',
+    `commission_min_guarantee` DECIMAL(10,2) DEFAULT NULL COMMENT '最低保底提成金额(元)',
+    `commission_period_months` INT DEFAULT 0 COMMENT '提成账期(延后N个月结算)',
+    `payment_cycle` VARCHAR(20) DEFAULT 'MONTHLY' COMMENT '计费方式:MONTHLY-月付, QUARTERLY-季度付, SEMI_ANNUAL-半年付, ANNUAL-年付',
+    `termination_date` DATE DEFAULT NULL COMMENT '解约日期',
+    `termination_remark` VARCHAR(500) DEFAULT NULL COMMENT '解约备注',
+    `payment_method` VARCHAR(20) DEFAULT NULL COMMENT '打款方式:CORPORATE-对公, PRIVATE-对私',
+    `payee_account_name` VARCHAR(100) DEFAULT NULL COMMENT '甲方收款账户名称',
+    `payee_bank_name` VARCHAR(100) DEFAULT NULL COMMENT '收款开户行',
+    `payee_bank_account` VARCHAR(50) DEFAULT NULL COMMENT '银行账号',
+    `taxpayer_id` VARCHAR(50) DEFAULT NULL COMMENT '纳税人识别号',
+    `invoice_type` VARCHAR(20) DEFAULT NULL COMMENT '发票类型:SPECIAL-增值税专票, NORMAL-增值税普票',
+    `invoice_tax_rate` DECIMAL(5,2) DEFAULT NULL COMMENT '开票税率(单位%)',
+    `creator_id` BIGINT DEFAULT NULL COMMENT '数据创建人ID',
+    `creator_name` VARCHAR(50) DEFAULT NULL COMMENT '数据创建人姓名',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `editor_id` BIGINT DEFAULT NULL COMMENT '最后编辑人ID',
+    `editor_name` VARCHAR(50) DEFAULT NULL COMMENT '最后编辑人姓名',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后编辑时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_shop_id` (`shop_id`),
+    KEY `idx_contract_no` (`contract_no`),
+    KEY `idx_contract_status` (`contract_status`),
+    KEY `idx_business_status` (`business_status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='门店档案主表';
+
+-- =====================================================
+-- 2. 门店固定资产设备表
+-- =====================================================
+CREATE TABLE IF NOT EXISTS `t_shop_fixed_asset` (
+    `id` BIGINT NOT NULL COMMENT '主键ID(雪花算法)',
+    `shop_archive_id` BIGINT NOT NULL COMMENT '关联门店档案ID(t_shop_archive.id)',
+    `shop_id` BIGINT NOT NULL COMMENT '关联门店ID(冗余,便于查询)',
+    `asset_type` VARCHAR(20) NOT NULL COMMENT '资产类型:VENDING_MACHINE-自助售卖机, MICROWAVE-微波炉',
+    `asset_name` VARCHAR(100) DEFAULT NULL COMMENT '资产名称(如售卖机名称)',
+    `asset_code` VARCHAR(100) DEFAULT NULL COMMENT '资产编号(机器编号/微波炉编号)',
+    `asset_status` VARCHAR(20) DEFAULT 'NORMAL' COMMENT '设备当前状态:NORMAL-正常运行, REPAIR-故障维修, IDLE-闲置撤机',
+    `remark` VARCHAR(200) DEFAULT NULL COMMENT '备注',
+    `creator_id` BIGINT DEFAULT NULL COMMENT '创建人ID',
+    `creator_name` VARCHAR(50) DEFAULT NULL COMMENT '创建人姓名',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `editor_id` BIGINT DEFAULT NULL COMMENT '最后编辑人ID',
+    `editor_name` VARCHAR(50) DEFAULT NULL COMMENT '最后编辑人姓名',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后编辑时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_shop_archive_id` (`shop_archive_id`),
+    KEY `idx_shop_id` (`shop_id`),
+    KEY `idx_asset_type` (`asset_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='门店固定资产设备表';
+
+-- =====================================================
+-- 3. 门店合同附件表
+-- =====================================================
+CREATE TABLE IF NOT EXISTS `t_shop_contract_attachment` (
+    `id` BIGINT NOT NULL COMMENT '主键ID(雪花算法)',
+    `shop_archive_id` BIGINT NOT NULL COMMENT '关联门店档案ID(t_shop_archive.id)',
+    `shop_id` BIGINT NOT NULL COMMENT '关联门店ID(冗余,便于查询)',
+    `attachment_type` VARCHAR(30) NOT NULL COMMENT '附件类型:CONTRACT-合同附件, LEASE_AGREEMENT-场地租赁协议, ELECTRICITY_AGREEMENT-电费补充协议, DEPOSIT_RECEIPT-保证金收据, TERMINATION_AGREEMENT-解约/补充变更协议',
+    `file_name` VARCHAR(200) NOT NULL COMMENT '原始文件名',
+    `file_url` VARCHAR(500) NOT NULL COMMENT 'OSS文件URL',
+    `file_size` BIGINT DEFAULT NULL COMMENT '文件大小(字节)',
+    `file_extension` VARCHAR(20) DEFAULT NULL COMMENT '文件扩展名(pdf, jpg, png, docx等)',
+    `sort_order` INT DEFAULT 0 COMMENT '排序序号',
+    `creator_id` BIGINT DEFAULT NULL COMMENT '上传人ID',
+    `creator_name` VARCHAR(50) DEFAULT NULL COMMENT '上传人姓名',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_shop_archive_id` (`shop_archive_id`),
+    KEY `idx_shop_id` (`shop_id`),
+    KEY `idx_attachment_type` (`attachment_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='门店合同附件表';
+
+-- =====================================================
+-- 4. 菜单数据(运营管理 → 门店档案管理)
+-- =====================================================
+INSERT INTO t_menu (id, parent_id, menu_type, title, name, path, component, `rank`, show_link, show_parent)
+VALUES (306, 300, 0, '门店档案管理', 'ShopArchive', '/operation/shop-archive', '', 5, 1, 0);
+
+-- =====================================================
+-- 5. 权限按钮(挂在 门店档案管理 下,menu_type=3)
+-- =====================================================
+INSERT INTO t_menu (id, parent_id, menu_type, title, name, path, component, `rank`, auths, show_link, show_parent) VALUES
+(30601, 306, 3, '查看', 'ShopArchiveRead',    '', '', 1, 'shop:archive:read',   1, 0),
+(30602, 306, 3, '创建', 'ShopArchiveCreate',  '', '', 2, 'shop:archive:create', 1, 0),
+(30603, 306, 3, '编辑', 'ShopArchiveUpdate',  '', '', 3, 'shop:archive:update', 1, 0),
+(30604, 306, 3, '删除', 'ShopArchiveDelete',  '', '', 4, 'shop:archive:delete', 1, 0);
+
+-- =====================================================
+-- 6. admin 角色(role_id=1)关联新菜单与权限按钮
+-- =====================================================
+INSERT INTO t_role_menu (id, role_id, menu_id) VALUES
+(90306, 1, 306),
+(90307, 1, 30601),
+(90308, 1, 30602),
+(90309, 1, 30603),
+(90310, 1, 30604);

+ 1928 - 0
docs/门店档案模块.md

@@ -0,0 +1,1928 @@
+# 门店档案模块设计文档
+
+> 本文档为门店档案功能的完整需求设计,涵盖数据库设计、实体类、枚举、API 接口、OSS 文件上传方案。
+> 门店档案用于管理门店的基础信息、固定资产、合同信息、结算开票信息及合同附件。
+
+---
+
+## 一、功能概述
+
+### 1.1 业务背景
+
+现有 `t_shop` 表仅包含门店基础字段(名称、地址、联系人等),无法满足业务上对门店合同、租金、结算、附件等档案信息的管理需求。本模块在现有门店体系基础上扩展,新增门店档案主表、固定资产子表、附件子表,实现门店全生命周期档案管理。
+
+### 1.2 与现有模块的关系
+
+| 现有模块 | 关系说明 |
+|---------|---------|
+| `t_shop`(门店表) | 门店档案通过 `shop_id` 关联,一个门店对应一条档案记录 |
+| `t_device`(设备表) | 固定资产中的"自助售卖机"与设备表独立管理,档案中的设备信息侧重商务属性(机器编号、状态),不替代设备表的技术属性 |
+| OssUploadController | 新增 OSS 上传凭证接口,前端获取 STS Token 后直传 OSS,后端仅保存 URL |
+
+### 1.3 模块划分
+
+```
+门店档案
+├── 一、门店基础信息(档案主表字段)
+├── 二、固定资产设备信息(固定资产子表,1:N)
+├── 三、合同核心信息(档案主表字段)
+├── 四、甲方收款结算 & 开票信息(档案主表字段)
+├── 五、合同附件归档(附件子表,1:N)
+└── 六、系统溯源字段(自动回填)
+```
+
+---
+
+## 二、数据库设计
+
+### 2.1 门店档案主表 `t_shop_archive`
+
+```sql
+CREATE TABLE IF NOT EXISTS `t_shop_archive` (
+    `id` BIGINT NOT NULL COMMENT '主键ID(雪花算法)',
+
+    -- ========== 关联 ==========
+    `shop_id` BIGINT NOT NULL COMMENT '关联门店ID(t_shop.id)',
+
+    -- ========== 一、门店基础信息 ==========
+    `shop_name` VARCHAR(100) NOT NULL COMMENT '门店名称(冗余,便于查询)',
+    `shop_address` VARCHAR(255) DEFAULT NULL COMMENT '门店地址(完整详细地址)',
+    `venue_type` VARCHAR(20) DEFAULT NULL COMMENT '合作场地类型:CHARGING_STATION-充电站, OFFICE_BUILDING-写字楼',
+    `cooperation_mode` VARCHAR(100) DEFAULT NULL COMMENT '合作模式(多选逗号分隔):DRINKS-水饮副食, BOX_MEAL-盒饭',
+
+    `party_b_salesman` VARCHAR(50) DEFAULT NULL COMMENT '乙方业务员姓名',
+    `party_b_salesman_phone` VARCHAR(20) DEFAULT NULL COMMENT '乙方业务员手机号',
+
+    `party_a_company` VARCHAR(200) DEFAULT NULL COMMENT '甲方公司名称(签约主体全称)',
+    `party_a_manager` VARCHAR(50) DEFAULT NULL COMMENT '甲方场地管理人员姓名',
+    `party_a_manager_phone` VARCHAR(20) DEFAULT NULL COMMENT '甲方场地管理人员电话',
+
+    `business_status` VARCHAR(20) DEFAULT 'NORMAL' COMMENT '门店经营状态:NORMAL-正常营业, SUSPENDED-暂停营业, WITHDRAWN-已撤场',
+    `remark` VARCHAR(500) DEFAULT NULL COMMENT '门店备注(特殊要求等)',
+
+    -- ========== 三、合同核心信息 ==========
+    `contract_no` VARCHAR(100) DEFAULT NULL COMMENT '合同编号(企业归档唯一编码)',
+    `contract_sign_date` DATE DEFAULT NULL COMMENT '合同签订日期',
+    `contract_start_date` DATE DEFAULT NULL COMMENT '合同起始日',
+    `contract_end_date` DATE DEFAULT NULL COMMENT '合同到期日',
+    `contract_status` VARCHAR(20) DEFAULT 'PENDING' COMMENT '合同状态:PENDING-待签约, ACTIVE-已生效, EXPIRING-到期待续约, TERMINATED-已解约终止',
+
+    `rent_start_date` DATE DEFAULT NULL COMMENT '租金起始日',
+    `monthly_rent` DECIMAL(10,2) DEFAULT NULL COMMENT '每月固定租金金额(元)',
+    `rent_free_days` INT DEFAULT 0 COMMENT '免租期天数',
+    `rent_pay_deadline_day` INT DEFAULT NULL COMMENT '租金缴纳截止日(每月几号)',
+
+    `deposit_amount` DECIMAL(10,2) DEFAULT NULL COMMENT '保证金(合同约定总额,元)',
+    `deposit_status` VARCHAR(20) DEFAULT 'UNPAID' COMMENT '保证金缴纳状态:UNPAID-未缴纳, PARTIAL-部分缴纳, FULL-全额缴纳',
+    `deposit_paid_amount` DECIMAL(10,2) DEFAULT NULL COMMENT '保证金实缴金额(元)',
+    `deposit_refund_condition` VARCHAR(500) DEFAULT NULL COMMENT '保证金退还条件',
+
+    `billing_mode` VARCHAR(20) DEFAULT 'RENT_ONLY' COMMENT '合作计费模式:RENT_ONLY-纯租金, RENT_ELECTRIC-租金+电费, COMMISSION_ONLY-纯提成, RENT_COMMISSION-租金+提成, RENT_COMMISSION_ELECTRIC-租金+提成+电费',
+    `electricity_price` DECIMAL(8,4) DEFAULT NULL COMMENT '电费结算单价(元/度)',
+    `electricity_share_rule` VARCHAR(500) DEFAULT NULL COMMENT '电费公摊规则说明',
+
+    `commission_base_type` VARCHAR(20) DEFAULT NULL COMMENT '提成基数计算口径:SALES-门店实收销售额, GROSS_PROFIT-门店毛利总额, NET_REVENUE-扣除成本后营收',
+    `commission_base` DECIMAL(12,2) DEFAULT NULL COMMENT '提成基数(元)',
+    `commission_rate` DECIMAL(5,2) DEFAULT NULL COMMENT '提成比例(0~100,单位%)',
+    `commission_min_guarantee` DECIMAL(10,2) DEFAULT NULL COMMENT '最低保底提成金额(元)',
+    `commission_period_months` INT DEFAULT 0 COMMENT '提成账期(延后N个月结算)',
+
+    `payment_cycle` VARCHAR(20) DEFAULT 'MONTHLY' COMMENT '计费方式:MONTHLY-月付, QUARTERLY-季度付, SEMI_ANNUAL-半年付, ANNUAL-年付',
+
+    `termination_date` DATE DEFAULT NULL COMMENT '解约日期',
+    `termination_remark` VARCHAR(500) DEFAULT NULL COMMENT '解约备注',
+
+    -- ========== 四、甲方收款结算 & 开票信息 ==========
+    `payment_method` VARCHAR(20) DEFAULT NULL COMMENT '打款方式:CORPORATE-对公, PRIVATE-对私',
+    `payee_account_name` VARCHAR(100) DEFAULT NULL COMMENT '甲方收款账户名称',
+    `payee_bank_name` VARCHAR(100) DEFAULT NULL COMMENT '收款开户行',
+    `payee_bank_account` VARCHAR(50) DEFAULT NULL COMMENT '银行账号',
+    `taxpayer_id` VARCHAR(50) DEFAULT NULL COMMENT '纳税人识别号',
+    `invoice_type` VARCHAR(20) DEFAULT NULL COMMENT '发票类型:SPECIAL-增值税专票, NORMAL-增值税普票',
+    `invoice_tax_rate` DECIMAL(5,2) DEFAULT NULL COMMENT '开票税率(单位%)',
+
+    -- ========== 六、系统溯源字段 ==========
+    `creator_id` BIGINT DEFAULT NULL COMMENT '数据创建人ID',
+    `creator_name` VARCHAR(50) DEFAULT NULL COMMENT '数据创建人姓名',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `editor_id` BIGINT DEFAULT NULL COMMENT '最后编辑人ID',
+    `editor_name` VARCHAR(50) DEFAULT NULL COMMENT '最后编辑人姓名',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后编辑时间',
+
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_shop_id` (`shop_id`),
+    KEY `idx_contract_no` (`contract_no`),
+    KEY `idx_contract_status` (`contract_status`),
+    KEY `idx_business_status` (`business_status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='门店档案主表';
+```
+
+### 2.2 固定资产设备表 `t_shop_fixed_asset`
+
+```sql
+CREATE TABLE IF NOT EXISTS `t_shop_fixed_asset` (
+    `id` BIGINT NOT NULL COMMENT '主键ID(雪花算法)',
+
+    `shop_archive_id` BIGINT NOT NULL COMMENT '关联门店档案ID(t_shop_archive.id)',
+    `shop_id` BIGINT NOT NULL COMMENT '关联门店ID(冗余,便于查询)',
+
+    `asset_type` VARCHAR(20) NOT NULL COMMENT '资产类型:VENDING_MACHINE-自助售卖机, MICROWAVE-微波炉',
+    `asset_name` VARCHAR(100) DEFAULT NULL COMMENT '资产名称(如售卖机名称)',
+    `asset_code` VARCHAR(100) DEFAULT NULL COMMENT '资产编号(机器编号/微波炉编号)',
+    `asset_status` VARCHAR(20) DEFAULT 'NORMAL' COMMENT '设备当前状态:NORMAL-正常运行, REPAIR-故障维修, IDLE-闲置撤机',
+
+    `remark` VARCHAR(200) DEFAULT NULL COMMENT '备注',
+
+    `creator_id` BIGINT DEFAULT NULL COMMENT '创建人ID',
+    `creator_name` VARCHAR(50) DEFAULT NULL COMMENT '创建人姓名',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `editor_id` BIGINT DEFAULT NULL COMMENT '最后编辑人ID',
+    `editor_name` VARCHAR(50) DEFAULT NULL COMMENT '最后编辑人姓名',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后编辑时间',
+
+    PRIMARY KEY (`id`),
+    KEY `idx_shop_archive_id` (`shop_archive_id`),
+    KEY `idx_shop_id` (`shop_id`),
+    KEY `idx_asset_type` (`asset_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='门店固定资产设备表';
+```
+
+### 2.3 合同附件表 `t_shop_contract_attachment`
+
+```sql
+CREATE TABLE IF NOT EXISTS `t_shop_contract_attachment` (
+    `id` BIGINT NOT NULL COMMENT '主键ID(雪花算法)',
+
+    `shop_archive_id` BIGINT NOT NULL COMMENT '关联门店档案ID(t_shop_archive.id)',
+    `shop_id` BIGINT NOT NULL COMMENT '关联门店ID(冗余,便于查询)',
+
+    `attachment_type` VARCHAR(30) NOT NULL COMMENT '附件类型:CONTRACT-合同附件, LEASE_AGREEMENT-场地租赁协议, ELECTRICITY_AGREEMENT-电费补充协议, DEPOSIT_RECEIPT-保证金收据, TERMINATION_AGREEMENT-解约/补充变更协议',
+    `file_name` VARCHAR(200) NOT NULL COMMENT '原始文件名',
+    `file_url` VARCHAR(500) NOT NULL COMMENT 'OSS文件URL',
+    `file_size` BIGINT DEFAULT NULL COMMENT '文件大小(字节)',
+    `file_extension` VARCHAR(20) DEFAULT NULL COMMENT '文件扩展名(pdf, jpg, png, docx等)',
+
+    `sort_order` INT DEFAULT 0 COMMENT '排序序号',
+
+    `creator_id` BIGINT DEFAULT NULL COMMENT '上传人ID',
+    `creator_name` VARCHAR(50) DEFAULT NULL COMMENT '上传人姓名',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',
+
+    PRIMARY KEY (`id`),
+    KEY `idx_shop_archive_id` (`shop_archive_id`),
+    KEY `idx_shop_id` (`shop_id`),
+    KEY `idx_attachment_type` (`attachment_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='门店合同附件表';
+```
+
+---
+
+## 三、枚举类设计
+
+### 3.1 合作场地类型 `VenueType`
+
+**位置**: `haha-common/src/main/java/com/haha/common/enums/VenueType.java`
+
+```java
+package com.haha.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum VenueType {
+
+    CHARGING_STATION("CHARGING_STATION", "充电站"),
+    OFFICE_BUILDING("OFFICE_BUILDING", "写字楼");
+
+    private final String code;
+    private final String description;
+
+    VenueType(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public static VenueType fromCode(String code) {
+        if (code == null) return null;
+        for (VenueType t : values()) {
+            if (t.code.equals(code)) return t;
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        VenueType t = fromCode(code);
+        return t != null ? t.description : "未知";
+    }
+}
+```
+
+### 3.2 门店经营状态 `BusinessStatus`
+
+**位置**: `haha-common/src/main/java/com/haha/common/enums/BusinessStatus.java`
+
+```java
+package com.haha.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum BusinessStatus {
+
+    NORMAL("NORMAL", "正常营业"),
+    SUSPENDED("SUSPENDED", "暂停营业"),
+    WITHDRAWN("WITHDRAWN", "已撤场");
+
+    private final String code;
+    private final String description;
+
+    BusinessStatus(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public static BusinessStatus fromCode(String code) {
+        if (code == null) return null;
+        for (BusinessStatus s : values()) {
+            if (s.code.equals(code)) return s;
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        BusinessStatus s = fromCode(code);
+        return s != null ? s.description : "未知";
+    }
+}
+```
+
+### 3.3 合同状态 `ContractStatus`
+
+**位置**: `haha-common/src/main/java/com/haha/common/enums/ContractStatus.java`
+
+```java
+package com.haha.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum ContractStatus {
+
+    PENDING("PENDING", "待签约"),
+    ACTIVE("ACTIVE", "已生效"),
+    EXPIRING("EXPIRING", "到期待续约"),
+    TERMINATED("TERMINATED", "已解约终止");
+
+    private final String code;
+    private final String description;
+
+    ContractStatus(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public static ContractStatus fromCode(String code) {
+        if (code == null) return null;
+        for (ContractStatus s : values()) {
+            if (s.code.equals(code)) return s;
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        ContractStatus s = fromCode(code);
+        return s != null ? s.description : "未知";
+    }
+}
+```
+
+### 3.4 保证金缴纳状态 `DepositStatus`
+
+**位置**: `haha-common/src/main/java/com/haha/common/enums/DepositStatus.java`
+
+```java
+package com.haha.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum DepositStatus {
+
+    UNPAID("UNPAID", "未缴纳"),
+    PARTIAL("PARTIAL", "部分缴纳"),
+    FULL("FULL", "全额缴纳");
+
+    private final String code;
+    private final String description;
+
+    DepositStatus(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public static DepositStatus fromCode(String code) {
+        if (code == null) return null;
+        for (DepositStatus s : values()) {
+            if (s.code.equals(code)) return s;
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        DepositStatus s = fromCode(code);
+        return s != null ? s.description : "未知";
+    }
+}
+```
+
+### 3.5 计费模式 `BillingMode`
+
+**位置**: `haha-common/src/main/java/com/haha/common/enums/BillingMode.java`
+
+```java
+package com.haha.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum BillingMode {
+
+    RENT_ONLY("RENT_ONLY", "纯租金"),
+    RENT_ELECTRIC("RENT_ELECTRIC", "租金+电费"),
+    COMMISSION_ONLY("COMMISSION_ONLY", "纯提成"),
+    RENT_COMMISSION("RENT_COMMISSION", "租金+提成"),
+    RENT_COMMISSION_ELECTRIC("RENT_COMMISSION_ELECTRIC", "租金+提成+电费");
+
+    private final String code;
+    private final String description;
+
+    BillingMode(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public static BillingMode fromCode(String code) {
+        if (code == null) return null;
+        for (BillingMode m : values()) {
+            if (m.code.equals(code)) return m;
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        BillingMode m = fromCode(code);
+        return m != null ? m.description : "未知";
+    }
+}
+```
+
+### 3.6 计费方式 `PaymentCycle`
+
+**位置**: `haha-common/src/main/java/com/haha/common/enums/PaymentCycle.java`
+
+```java
+package com.haha.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum PaymentCycle {
+
+    MONTHLY("MONTHLY", "月付"),
+    QUARTERLY("QUARTERLY", "季度付"),
+    SEMI_ANNUAL("SEMI_ANNUAL", "半年付"),
+    ANNUAL("ANNUAL", "年付");
+
+    private final String code;
+    private final String description;
+
+    PaymentCycle(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public static PaymentCycle fromCode(String code) {
+        if (code == null) return null;
+        for (PaymentCycle c : values()) {
+            if (c.code.equals(code)) return c;
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        PaymentCycle c = fromCode(code);
+        return c != null ? c.description : "未知";
+    }
+}
+```
+
+### 3.7 固定资产类型 `AssetType`
+
+**位置**: `haha-common/src/main/java/com/haha/common/enums/AssetType.java`
+
+```java
+package com.haha.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum AssetType {
+
+    VENDING_MACHINE("VENDING_MACHINE", "自助售卖机"),
+    MICROWAVE("MICROWAVE", "微波炉");
+
+    private final String code;
+    private final String description;
+
+    AssetType(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public static AssetType fromCode(String code) {
+        if (code == null) return null;
+        for (AssetType t : values()) {
+            if (t.code.equals(code)) return t;
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        AssetType t = fromCode(code);
+        return t != null ? t.description : "未知";
+    }
+}
+```
+
+### 3.8 资产设备状态 `AssetStatus`
+
+**位置**: `haha-common/src/main/java/com/haha/common/enums/AssetStatus.java`
+
+```java
+package com.haha.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum AssetStatus {
+
+    NORMAL("NORMAL", "正常运行"),
+    REPAIR("REPAIR", "故障维修"),
+    IDLE("IDLE", "闲置撤机");
+
+    private final String code;
+    private final String description;
+
+    AssetStatus(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public static AssetStatus fromCode(String code) {
+        if (code == null) return null;
+        for (AssetStatus s : values()) {
+            if (s.code.equals(code)) return s;
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        AssetStatus s = fromCode(code);
+        return s != null ? s.description : "未知";
+    }
+}
+```
+
+### 3.9 附件类型 `AttachmentType`
+
+**位置**: `haha-common/src/main/java/com/haha/common/enums/AttachmentType.java`
+
+```java
+package com.haha.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum AttachmentType {
+
+    CONTRACT("CONTRACT", "合同附件"),
+    LEASE_AGREEMENT("LEASE_AGREEMENT", "场地租赁协议"),
+    ELECTRICITY_AGREEMENT("ELECTRICITY_AGREEMENT", "电费补充协议"),
+    DEPOSIT_RECEIPT("DEPOSIT_RECEIPT", "保证金收据"),
+    TERMINATION_AGREEMENT("TERMINATION_AGREEMENT", "解约/补充变更协议");
+
+    private final String code;
+    private final String description;
+
+    AttachmentType(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public static AttachmentType fromCode(String code) {
+        if (code == null) return null;
+        for (AttachmentType t : values()) {
+            if (t.code.equals(code)) return t;
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        AttachmentType t = fromCode(code);
+        return t != null ? t.description : "未知";
+    }
+}
+```
+
+### 3.10 打款方式 `PaymentMethod`
+
+**位置**: `haha-common/src/main/java/com/haha/common/enums/PaymentMethod.java`
+
+```java
+package com.haha.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum PaymentMethod {
+
+    CORPORATE("CORPORATE", "对公"),
+    PRIVATE("PRIVATE", "对私");
+
+    private final String code;
+    private final String description;
+
+    PaymentMethod(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public static PaymentMethod fromCode(String code) {
+        if (code == null) return null;
+        for (PaymentMethod m : values()) {
+            if (m.code.equals(code)) return m;
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        PaymentMethod m = fromCode(code);
+        return m != null ? m.description : "未知";
+    }
+}
+```
+
+### 3.11 发票类型 `InvoiceType`
+
+**位置**: `haha-common/src/main/java/com/haha/common/enums/InvoiceType.java`
+
+```java
+package com.haha.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum InvoiceType {
+
+    SPECIAL("SPECIAL", "增值税专票"),
+    NORMAL("NORMAL", "增值税普票");
+
+    private final String code;
+    private final String description;
+
+    InvoiceType(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public static InvoiceType fromCode(String code) {
+        if (code == null) return null;
+        for (InvoiceType t : values()) {
+            if (t.code.equals(code)) return t;
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        InvoiceType t = fromCode(code);
+        return t != null ? t.description : "未知";
+    }
+}
+```
+
+### 3.12 提成基数计算口径 `CommissionBaseType`
+
+**位置**: `haha-common/src/main/java/com/haha/common/enums/CommissionBaseType.java`
+
+```java
+package com.haha.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum CommissionBaseType {
+
+    SALES("SALES", "门店实收销售额"),
+    GROSS_PROFIT("GROSS_PROFIT", "门店毛利总额"),
+    NET_REVENUE("NET_REVENUE", "扣除成本后营收");
+
+    private final String code;
+    private final String description;
+
+    CommissionBaseType(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public static CommissionBaseType fromCode(String code) {
+        if (code == null) return null;
+        for (CommissionBaseType t : values()) {
+            if (t.code.equals(code)) return t;
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        CommissionBaseType t = fromCode(code);
+        return t != null ? t.description : "未知";
+    }
+}
+```
+
+---
+
+## 四、实体类设计
+
+### 4.1 ShopArchive(门店档案主表实体)
+
+**位置**: `haha-entity/src/main/java/com/haha/entity/ShopArchive.java`
+
+```java
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@TableName("t_shop_archive")
+public class ShopArchive implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.ASSIGN_ID)
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    // ========== 关联 ==========
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long shopId;
+
+    // ========== 一、门店基础信息 ==========
+    private String shopName;
+    private String shopAddress;
+    private String venueType;
+    private String cooperationMode;
+    private String partyBSalesman;
+    private String partyBSalesmanPhone;
+    private String partyACompany;
+    private String partyAManager;
+    private String partyAManagerPhone;
+    private String businessStatus;
+    private String remark;
+
+    // ========== 三、合同核心信息 ==========
+    private String contractNo;
+
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
+    private LocalDate contractSignDate;
+
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
+    private LocalDate contractStartDate;
+
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
+    private LocalDate contractEndDate;
+
+    private String contractStatus;
+
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
+    private LocalDate rentStartDate;
+
+    private BigDecimal monthlyRent;
+    private Integer rentFreeDays;
+    private Integer rentPayDeadlineDay;
+
+    private BigDecimal depositAmount;
+    private String depositStatus;
+    private BigDecimal depositPaidAmount;
+    private String depositRefundCondition;
+
+    private String billingMode;
+    private BigDecimal electricityPrice;
+    private String electricityShareRule;
+
+    private String commissionBaseType;
+    private BigDecimal commissionBase;
+    private BigDecimal commissionRate;
+    private BigDecimal commissionMinGuarantee;
+    private Integer commissionPeriodMonths;
+
+    private String paymentCycle;
+
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
+    private LocalDate terminationDate;
+
+    private String terminationRemark;
+
+    // ========== 四、甲方收款结算 & 开票信息 ==========
+    private String paymentMethod;
+    private String payeeAccountName;
+    private String payeeBankName;
+    private String payeeBankAccount;
+    private String taxpayerId;
+    private String invoiceType;
+    private BigDecimal invoiceTaxRate;
+
+    // ========== 六、系统溯源字段 ==========
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long creatorId;
+    private String creatorName;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long editorId;
+    private String editorName;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime updateTime;
+
+    // ========== 非数据库字段 ==========
+
+    /** 关联的固定资产列表 */
+    @TableField(exist = false)
+    private List<ShopFixedAsset> fixedAssets;
+
+    /** 关联的附件列表 */
+    @TableField(exist = false)
+    private List<ShopContractAttachment> attachments;
+
+    /** 关联门店编码 */
+    @TableField(exist = false)
+    private String shopCode;
+
+    /** 合同状态标签 */
+    @TableField(exist = false)
+    private String contractStatusLabel;
+
+    /** 经营状态标签 */
+    @TableField(exist = false)
+    private String businessStatusLabel;
+}
+```
+
+### 4.2 ShopFixedAsset(固定资产设备实体)
+
+**位置**: `haha-entity/src/main/java/com/haha/entity/ShopFixedAsset.java`
+
+```java
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_shop_fixed_asset")
+public class ShopFixedAsset implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.ASSIGN_ID)
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long shopArchiveId;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long shopId;
+
+    private String assetType;
+    private String assetName;
+    private String assetCode;
+    private String assetStatus;
+    private String remark;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long creatorId;
+    private String creatorName;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long editorId;
+    private String editorName;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime updateTime;
+
+    // ========== 非数据库字段 ==========
+
+    /** 资产类型标签 */
+    @TableField(exist = false)
+    private String assetTypeLabel;
+
+    /** 资产状态标签 */
+    @TableField(exist = false)
+    private String assetStatusLabel;
+}
+```
+
+### 4.3 ShopContractAttachment(合同附件实体)
+
+**位置**: `haha-entity/src/main/java/com/haha/entity/ShopContractAttachment.java`
+
+```java
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_shop_contract_attachment")
+public class ShopContractAttachment implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.ASSIGN_ID)
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long shopArchiveId;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long shopId;
+
+    private String attachmentType;
+    private String fileName;
+    private String fileUrl;
+    private Long fileSize;
+    private String fileExtension;
+    private Integer sortOrder;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long creatorId;
+    private String creatorName;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+
+    // ========== 非数据库字段 ==========
+
+    /** 附件类型标签 */
+    @TableField(exist = false)
+    private String attachmentTypeLabel;
+}
+```
+
+---
+
+## 五、DTO 类设计
+
+### 5.1 ShopArchiveCreateDTO
+
+**位置**: `haha-entity/src/main/java/com/haha/entity/dto/ShopArchiveCreateDTO.java`
+
+```java
+package com.haha.entity.dto;
+
+import lombok.Data;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+
+@Data
+public class ShopArchiveCreateDTO {
+
+    @NotNull(message = "门店ID不能为空")
+    private Long shopId;
+
+    // 基础信息
+    @NotBlank(message = "门店名称不能为空")
+    private String shopName;
+    private String shopAddress;
+    private String venueType;
+    private String cooperationMode;
+    private String partyBSalesman;
+    private String partyBSalesmanPhone;
+    private String partyACompany;
+    private String partyAManager;
+    private String partyAManagerPhone;
+    private String businessStatus;
+    private String remark;
+
+    // 合同信息
+    private String contractNo;
+    private LocalDate contractSignDate;
+    private LocalDate contractStartDate;
+    private LocalDate contractEndDate;
+    private String contractStatus;
+    private LocalDate rentStartDate;
+    private BigDecimal monthlyRent;
+    private Integer rentFreeDays;
+    private Integer rentPayDeadlineDay;
+    private BigDecimal depositAmount;
+    private String depositStatus;
+    private BigDecimal depositPaidAmount;
+    private String depositRefundCondition;
+    private String billingMode;
+    private BigDecimal electricityPrice;
+    private String electricityShareRule;
+    private String commissionBaseType;
+    private BigDecimal commissionBase;
+    private BigDecimal commissionRate;
+    private BigDecimal commissionMinGuarantee;
+    private Integer commissionPeriodMonths;
+    private String paymentCycle;
+    private LocalDate terminationDate;
+    private String terminationRemark;
+
+    // 结算开票信息
+    private String paymentMethod;
+    private String payeeAccountName;
+    private String payeeBankName;
+    private String payeeBankAccount;
+    private String taxpayerId;
+    private String invoiceType;
+    private BigDecimal invoiceTaxRate;
+
+    // 固定资产列表
+    @Valid
+    private List<FixedAssetDTO> fixedAssets;
+
+    // 附件列表
+    @Valid
+    private List<AttachmentDTO> attachments;
+}
+```
+
+### 5.2 ShopArchiveUpdateDTO
+
+**位置**: `haha-entity/src/main/java/com/haha/entity/dto/ShopArchiveUpdateDTO.java`
+
+与 `ShopArchiveCreateDTO` 字段相同,额外增加:
+
+```java
+@NotNull(message = "档案ID不能为空")
+private Long id;
+```
+
+### 5.3 ShopArchiveQueryDTO
+
+**位置**: `haha-entity/src/main/java/com/haha/entity/dto/ShopArchiveQueryDTO.java`
+
+```java
+package com.haha.entity.dto;
+
+import com.haha.common.dto.BasePageQueryDTO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ShopArchiveQueryDTO extends BasePageQueryDTO {
+
+    private Long shopId;
+    private String shopName;
+    private String contractNo;
+    private String contractStatus;
+    private String businessStatus;
+    private String billingMode;
+    private String venueType;
+}
+```
+
+### 5.4 FixedAssetDTO(固定资产子项DTO)
+
+**位置**: `haha-entity/src/main/java/com/haha/entity/dto/FixedAssetDTO.java`
+
+```java
+package com.haha.entity.dto;
+
+import lombok.Data;
+import jakarta.validation.constraints.NotBlank;
+
+@Data
+public class FixedAssetDTO {
+
+    private Long id;  // 更新时传,新增时不传
+
+    @NotBlank(message = "资产类型不能为空")
+    private String assetType;
+
+    private String assetName;
+    private String assetCode;
+    private String assetStatus;
+    private String remark;
+}
+```
+
+### 5.5 AttachmentDTO(附件子项DTO)
+
+**位置**: `haha-entity/src/main/java/com/haha/entity/dto/AttachmentDTO.java`
+
+```java
+package com.haha.entity.dto;
+
+import lombok.Data;
+import jakarta.validation.constraints.NotBlank;
+
+@Data
+public class AttachmentDTO {
+
+    private Long id;  // 更新时传,新增时不传
+
+    @NotBlank(message = "附件类型不能为空")
+    private String attachmentType;
+
+    @NotBlank(message = "文件名不能为空")
+    private String fileName;
+
+    @NotBlank(message = "文件URL不能为空")
+    private String fileUrl;
+
+    private Long fileSize;
+    private String fileExtension;
+    private Integer sortOrder;
+}
+```
+
+---
+
+## 六、Mapper 接口设计
+
+### 6.1 ShopArchiveMapper
+
+**位置**: `haha-mapper/src/main/java/com/haha/mapper/ShopArchiveMapper.java`
+
+```java
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.ShopArchive;
+import org.apache.ibatis.annotations.Param;
+
+public interface ShopArchiveMapper extends BaseMapper<ShopArchive> {
+
+    /**
+     * 根据门店ID查询档案(含关联数据)
+     */
+    ShopArchive selectByShopId(@Param("shopId") Long shopId);
+}
+```
+
+### 6.2 ShopFixedAssetMapper
+
+**位置**: `haha-mapper/src/main/java/com/haha/mapper/ShopFixedAssetMapper.java`
+
+```java
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.ShopFixedAsset;
+
+public interface ShopFixedAssetMapper extends BaseMapper<ShopFixedAsset> {
+}
+```
+
+### 6.3 ShopContractAttachmentMapper
+
+**位置**: `haha-mapper/src/main/java/com/haha/mapper/ShopContractAttachmentMapper.java`
+
+```java
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.ShopContractAttachment;
+
+public interface ShopContractAttachmentMapper extends BaseMapper<ShopContractAttachment> {
+}
+```
+
+### 6.4 ShopArchiveMapper.xml
+
+**位置**: `haha-mapper/src/main/resources/mapper/ShopArchiveMapper.xml`
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.haha.mapper.ShopArchiveMapper">
+
+    <resultMap id="ShopArchiveResultMap" type="com.haha.entity.ShopArchive">
+        <id column="id" property="id"/>
+        <result column="shop_id" property="shopId"/>
+        <result column="shop_name" property="shopName"/>
+        <result column="shop_address" property="shopAddress"/>
+        <result column="venue_type" property="venueType"/>
+        <result column="cooperation_mode" property="cooperationMode"/>
+        <result column="party_b_salesman" property="partyBSalesman"/>
+        <result column="party_b_salesman_phone" property="partyBSalesmanPhone"/>
+        <result column="party_a_company" property="partyACompany"/>
+        <result column="party_a_manager" property="partyAManager"/>
+        <result column="party_a_manager_phone" property="partyAManagerPhone"/>
+        <result column="business_status" property="businessStatus"/>
+        <result column="remark" property="remark"/>
+        <result column="contract_no" property="contractNo"/>
+        <result column="contract_sign_date" property="contractSignDate"/>
+        <result column="contract_start_date" property="contractStartDate"/>
+        <result column="contract_end_date" property="contractEndDate"/>
+        <result column="contract_status" property="contractStatus"/>
+        <result column="rent_start_date" property="rentStartDate"/>
+        <result column="monthly_rent" property="monthlyRent"/>
+        <result column="rent_free_days" property="rentFreeDays"/>
+        <result column="rent_pay_deadline_day" property="rentPayDeadlineDay"/>
+        <result column="deposit_amount" property="depositAmount"/>
+        <result column="deposit_status" property="depositStatus"/>
+        <result column="deposit_paid_amount" property="depositPaidAmount"/>
+        <result column="deposit_refund_condition" property="depositRefundCondition"/>
+        <result column="billing_mode" property="billingMode"/>
+        <result column="electricity_price" property="electricityPrice"/>
+        <result column="electricity_share_rule" property="electricityShareRule"/>
+        <result column="commission_base_type" property="commissionBaseType"/>
+        <result column="commission_base" property="commissionBase"/>
+        <result column="commission_rate" property="commissionRate"/>
+        <result column="commission_min_guarantee" property="commissionMinGuarantee"/>
+        <result column="commission_period_months" property="commissionPeriodMonths"/>
+        <result column="payment_cycle" property="paymentCycle"/>
+        <result column="termination_date" property="terminationDate"/>
+        <result column="termination_remark" property="terminationRemark"/>
+        <result column="payment_method" property="paymentMethod"/>
+        <result column="payee_account_name" property="payeeAccountName"/>
+        <result column="payee_bank_name" property="payeeBankName"/>
+        <result column="payee_bank_account" property="payeeBankAccount"/>
+        <result column="taxpayer_id" property="taxpayerId"/>
+        <result column="invoice_type" property="invoiceType"/>
+        <result column="invoice_tax_rate" property="invoiceTaxRate"/>
+        <result column="creator_id" property="creatorId"/>
+        <result column="creator_name" property="creatorName"/>
+        <result column="create_time" property="createTime"/>
+        <result column="editor_id" property="editorId"/>
+        <result column="editor_name" property="editorName"/>
+        <result column="update_time" property="updateTime"/>
+    </resultMap>
+
+    <select id="selectByShopId" resultMap="ShopArchiveResultMap">
+        SELECT * FROM t_shop_archive
+        WHERE shop_id = #{shopId}
+        AND deleted = 0
+        LIMIT 1
+    </select>
+
+</mapper>
+```
+
+---
+
+## 七、Service 层设计
+
+### 7.1 ShopArchiveService 接口
+
+**位置**: `haha-service/src/main/java/com/haha/service/ShopArchiveService.java`
+
+```java
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.ShopArchive;
+import com.haha.entity.dto.ShopArchiveCreateDTO;
+import com.haha.entity.dto.ShopArchiveQueryDTO;
+import com.haha.entity.dto.ShopArchiveUpdateDTO;
+
+import java.util.List;
+
+public interface ShopArchiveService extends IService<ShopArchive> {
+
+    /**
+     * 分页查询门店档案列表
+     */
+    IPage<ShopArchive> getPage(ShopArchiveQueryDTO queryDTO);
+
+    /**
+     * 查询门店档案详情(含固定资产、附件)
+     */
+    ShopArchive getDetailById(Long id);
+
+    /**
+     * 根据门店ID查询档案
+     */
+    ShopArchive getByShopId(Long shopId);
+
+    /**
+     * 创建门店档案
+     */
+    ShopArchive create(ShopArchiveCreateDTO dto);
+
+    /**
+     * 更新门店档案
+     */
+    ShopArchive update(ShopArchiveUpdateDTO dto);
+
+    /**
+     * 删除门店档案(逻辑删除)
+     */
+    void delete(Long id);
+
+    /**
+     * 获取所有枚举选项(供前端下拉使用)
+     */
+    List<?> getEnumOptions(String enumType);
+}
+```
+
+### 7.2 ShopArchiveServiceImpl 核心逻辑
+
+**位置**: `haha-service/src/main/java/com/haha/service/impl/ShopArchiveServiceImpl.java`
+
+核心业务逻辑要点:
+
+1. **创建档案**:
+   - 校验 `shopId` 对应的门店是否存在
+   - 校验同一门店不可重复创建档案(`uk_shop_id` 唯一约束)
+   - 自动回填 `creatorId`、`creatorName`(从 Sa-Token 获取当前登录管理员)
+   - 事务内保存主表 + 批量保存固定资产 + 批量保存附件
+
+2. **更新档案**:
+   - 自动回填 `editorId`、`editorName`
+   - 固定资产采用"先删后增"策略(根据 `shopArchiveId` 删除旧记录,重新插入)
+   - 附件采用"差异对比"策略(根据 ID 判断新增/更新/删除)
+   - 提成比例校验:0~100 区间
+
+3. **删除档案**:
+   - 逻辑删除(添加 `deleted` 字段,或在应用层标记状态)
+   - 同步删除关联的固定资产和附件记录
+
+4. **条件显示逻辑**(前端根据以下规则控制字段显隐):
+   - `billingMode` 包含电费相关(`RENT_ELECTRIC` / `RENT_COMMISSION_ELECTRIC`)时,显示电费单价和公摊规则
+   - `billingMode` 包含提成相关(`COMMISSION_ONLY` / `RENT_COMMISSION` / `RENT_COMMISSION_ELECTRIC`)时,显示提成基数、比例、保底金额、账期
+   - `paymentMethod` = `CORPORATE` 时,开户行必填
+
+---
+
+## 八、Controller 层设计
+
+**位置**: `haha-admin/src/main/java/com/haha/admin/controller/ShopArchiveController.java`
+
+```java
+@Slf4j
+@RestController
+@RequestMapping("/shop-archive")
+@RequiredArgsConstructor
+public class ShopArchiveController {
+
+    private final ShopArchiveService shopArchiveService;
+
+    /**
+     * 分页查询门店档案列表
+     */
+    @RequirePermission("shop:archive:read")
+    @GetMapping("/list")
+    public Result<PageResult<ShopArchive>> list(ShopArchiveQueryDTO queryDTO) {
+        queryDTO.validate();
+        return Result.success("查询成功", PageResult.of(shopArchiveService.getPage(queryDTO)));
+    }
+
+    /**
+     * 查询门店档案详情
+     */
+    @RequirePermission("shop:archive:read")
+    @GetMapping("/{id}")
+    public Result<ShopArchive> getById(@PathVariable Long id) {
+        ShopArchive archive = shopArchiveService.getDetailById(id);
+        if (archive == null) {
+            return Result.error(404, "门店档案不存在");
+        }
+        return Result.success("查询成功", archive);
+    }
+
+    /**
+     * 根据门店ID查询档案
+     */
+    @RequirePermission("shop:archive:read")
+    @GetMapping("/by-shop/{shopId}")
+    public Result<ShopArchive> getByShopId(@PathVariable Long shopId) {
+        ShopArchive archive = shopArchiveService.getByShopId(shopId);
+        if (archive == null) {
+            return Result.error(404, "该门店尚未创建档案");
+        }
+        return Result.success("查询成功", archive);
+    }
+
+    /**
+     * 创建门店档案
+     */
+    @RequirePermission("shop:archive:create")
+    @Log(module = "门店档案管理", operation = OperationType.CREATE, summary = "创建门店档案")
+    @PostMapping
+    public Result<ShopArchive> create(@RequestBody @Valid ShopArchiveCreateDTO dto) {
+        ShopArchive archive = shopArchiveService.create(dto);
+        return Result.success("创建成功", archive);
+    }
+
+    /**
+     * 更新门店档案
+     */
+    @RequirePermission("shop:archive:update")
+    @Log(module = "门店档案管理", operation = OperationType.UPDATE, summary = "更新门店档案")
+    @PutMapping("/{id}")
+    public Result<ShopArchive> update(@PathVariable Long id, @RequestBody @Valid ShopArchiveUpdateDTO dto) {
+        dto.setId(id);
+        ShopArchive archive = shopArchiveService.update(dto);
+        return Result.success("更新成功", archive);
+    }
+
+    /**
+     * 删除门店档案
+     */
+    @RequirePermission("shop:archive:delete")
+    @Log(module = "门店档案管理", operation = OperationType.DELETE, summary = "删除门店档案")
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable Long id) {
+        shopArchiveService.delete(id);
+        return Result.success("删除成功", null);
+    }
+
+    /**
+     * 获取枚举选项(供前端下拉框使用)
+     * @param enumType 枚举类型:venueType / businessStatus / contractStatus / depositStatus / billingMode / paymentCycle / assetType / assetStatus / attachmentType / paymentMethod / invoiceType / commissionBaseType
+     */
+    @RequirePermission("shop:archive:read")
+    @GetMapping("/enum-options")
+    public Result<List<StatusLabel>> getEnumOptions(@RequestParam String enumType) {
+        return Result.success("查询成功", shopArchiveService.getEnumOptions(enumType));
+    }
+}
+```
+
+### 8.1 API 接口汇总
+
+| 操作 | Method | 路径 | 权限 | 说明 |
+|------|--------|------|------|------|
+| 分页列表 | GET | `/admin/shop-archive/list` | `shop:archive:read` | 支持门店名称、合同编号、状态筛选 |
+| 详情 | GET | `/admin/shop-archive/{id}` | `shop:archive:read` | 含固定资产、附件 |
+| 按门店查询 | GET | `/admin/shop-archive/by-shop/{shopId}` | `shop:archive:read` | 根据门店ID查档案 |
+| 创建 | POST | `/admin/shop-archive` | `shop:archive:create` | 创建完整档案 |
+| 更新 | PUT | `/admin/shop-archive/{id}` | `shop:archive:update` | 更新档案信息 |
+| 删除 | DELETE | `/admin/shop-archive/{id}` | `shop:archive:delete` | 逻辑删除 |
+| 枚举选项 | GET | `/admin/shop-archive/enum-options?enumType=xxx` | `shop:archive:read` | 获取下拉选项 |
+| **获取上传凭证** | GET | `/admin/oss/upload-credential?dir=xxx` | `shop:archive:create` | 返回 STS Token,前端直传 OSS |
+| **删除 OSS 文件** | DELETE | `/admin/oss/file?objectKey=xxx` | `shop:archive:delete` | 根据 objectKey 删除 OSS 文件 |
+
+---
+
+## 九、阿里云 OSS 文件上传方案(前端直传)
+
+### 9.1 方案说明
+
+采用**前端直传 OSS** 模式,文件不经过后端服务器中转:
+
+```
+──────────┐     ①请求上传凭证      ┌──────────┐
+│  前端     │ ──────────────────────→ │  后端     │
+│          │ ←────────────────────── │          │
+│          │     ②返回STS Token       │          │
+│          │                          │          │
+│          │     ③直传文件到OSS        │          │
+│          │ ──────────────────────→ 阿里云 OSS  │
+│          │                          │          │
+│          │     ④上传成功,回调后端    │          │
+│          │ ──────────────────────→ │  后端     │
+│          │     保存 fileUrl         │  保存URL  │
+└──────────┘                          └──────────┘
+```
+
+**优势**:
+- 后端不处理文件流,减轻服务器带宽和存储压力
+- 前端直传速度更快,用户体验更好
+- 通过 STS 临时凭证控制权限,安全性有保障
+
+### 9.2 依赖引入
+
+在 `haha-service/pom.xml` 中添加:
+
+```xml
+<!-- 阿里云 STS SDK(用于生成临时上传凭证) -->
+<dependency>
+    <groupId>com.aliyun</groupId>
+    <artifactId>aliyun-java-sdk-sts</artifactId>
+    <version>3.1.2</version>
+</dependency>
+<dependency>
+    <groupId>com.aliyun</groupId>
+    <artifactId>aliyun-java-sdk-core</artifactId>
+    <version>4.6.4</version>
+</dependency>
+```
+
+### 9.3 配置项
+
+在 `application.yml` 中添加:
+
+```yaml
+# 阿里云 OSS 配置
+aliyun:
+  oss:
+    endpoint: oss-cn-hangzhou.aliyuncs.com                    # OSS Endpoint
+    access-key-id: ${OSS_ACCESS_KEY_ID}                        # 主账号AK(仅用于签发STS)
+    access-key-secret: ${OSS_ACCESS_KEY_SECRET}                # 主账号SK(仅用于签发STS)
+    bucket-name: haha-shop-archive                             # Bucket 名称
+    domain-prefix: https://haha-shop-archive.oss-cn-hangzhou.aliyuncs.com  # 访问域名前缀
+    sts:
+      role-arn: acs:ram::xxx:role/haha-oss-upload-role         # RAM角色ARN
+      role-session-name: haha-shop-archive-upload              # 会话名称
+      duration-seconds: 900                                    # 凭证有效期(秒),默认15分钟
+```
+
+### 9.4 OSS 配置类
+
+**位置**: `haha-common/src/main/java/com/haha/common/config/OssConfig.java`
+
+```java
+package com.haha.common.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Data
+@Configuration(proxyBeanMethods = false)
+@ConfigurationProperties(prefix = "aliyun.oss")
+public class OssConfig {
+
+    private String endpoint;
+    private String accessKeyId;
+    private String accessKeySecret;
+    private String bucketName;
+    private String domainPrefix;
+    private StsConfig sts = new StsConfig();
+
+    @Data
+    public static class StsConfig {
+        private String roleArn;
+        private String roleSessionName;
+        private int durationSeconds = 900;
+    }
+}
+```
+
+### 9.5 OssService 接口与实现
+
+**接口位置**: `haha-service/src/main/java/com/haha/service/OssService.java`
+
+```java
+package com.haha.service;
+
+import java.util.Map;
+
+public interface OssService {
+
+    /**
+     * 获取前端直传 OSS 的临时凭证(STS Token)
+     *
+     * @param dir 上传目录前缀(如 "shop-archive/contract")
+     * @return 包含 accessKeyId、accessKeySecret、securityToken、
+     *         expiration、host、dir 的凭证信息
+     */
+    Map<String, String> getUploadCredential(String dir);
+
+    /**
+     * 根据 objectKey 删除 OSS 文件
+     *
+     * @param objectKey OSS 对象键(不含域名)
+     */
+    void deleteByObjectKey(String objectKey);
+}
+```
+
+**实现位置**: `haha-service/src/main/java/com/haha/service/impl/OssServiceImpl.java`
+
+核心逻辑:
+- `getUploadCredential()`:调用阿里云 STS AssumeRole 接口,生成临时凭证
+  - 返回 `accessKeyId`、`accessKeySecret`、`securityToken`、`expiration`(过期时间)
+  - 返回 `host`(bucket 上传地址,如 `https://haha-shop-archive.oss-cn-hangzhou.aliyuncs.com`)
+  - 返回 `dir`(上传目录,如 `shop-archive/contract/20260709/`)
+  - 前端拿到凭证后使用阿里云 OSS SDK(`ali-oss` npm 包)直传
+- 凭证有效期 15 分钟,过期后前端需重新请求
+- 前端上传成功后,将 `fileUrl`(`domainPrefix + objectKey`)提交给后端保存
+
+### 9.6 上传凭证 Controller
+
+**位置**: `haha-admin/src/main/java/com/haha/admin/controller/OssUploadController.java`
+
+```java
+@Slf4j
+@RestController
+@RequestMapping("/oss")
+@RequiredArgsConstructor
+public class OssUploadController {
+
+    private final OssService ossService;
+
+    /**
+     * 获取 OSS 直传凭证
+     *
+     * @param dir 上传目录(如 contract、lease、electricity、deposit、termination)
+     * @return STS 临时凭证 + 上传地址
+     */
+    @RequirePermission("shop:archive:create")
+    @GetMapping("/upload-credential")
+    public Result<Map<String, String>> getUploadCredential(
+            @RequestParam(value = "dir", defaultValue = "shop-archive") String dir) {
+        Map<String, String> credential = ossService.getUploadCredential(dir);
+        return Result.success("获取凭证成功", credential);
+    }
+
+    /**
+     * 删除 OSS 文件(根据 objectKey)
+     */
+    @RequirePermission("shop:archive:delete")
+    @DeleteMapping("/file")
+    public Result<Void> deleteFile(@RequestParam String objectKey) {
+        ossService.deleteByObjectKey(objectKey);
+        return Result.success("删除成功", null);
+    }
+}
+```
+
+### 9.7 前端上传方案
+
+#### 9.7.1 安装依赖
+
+```bash
+npm install ali-oss
+```
+
+#### 9.7.2 上传工具类
+
+**位置**: `haha-admin-web/src/utils/ossUpload.ts`
+
+```typescript
+import OSS from 'ali-oss';
+import { http } from '@/utils/http';
+
+interface OssCredential {
+  accessKeyId: string;
+  accessKeySecret: string;
+  securityToken: string;
+  expiration: string;
+  host: string;
+  dir: string;
+}
+
+interface UploadResult {
+  url: string;       // 完整访问URL(domainPrefix + objectKey)
+  objectKey: string; // OSS对象键
+  fileName: string;
+  fileSize: number;
+  fileExtension: string;
+}
+
+let cachedCredential: OssCredential | null = null;
+let credentialExpireTime = 0;
+
+/**
+ * 获取 OSS 上传凭证(带缓存,过期前60秒自动刷新)
+ */
+async function getOssCredential(dir: string): Promise<OssCredential> {
+  const now = Date.now();
+  if (cachedCredential && credentialExpireTime - now > 60000) {
+    return cachedCredential;
+  }
+
+  const res: any = await http.request('get', '/oss/upload-credential', {
+    params: { dir }
+  });
+
+  if (res.code !== 200) {
+    throw new Error(res.message || '获取上传凭证失败');
+  }
+
+  cachedCredential = res.data;
+  credentialExpireTime = new Date(res.data.expiration).getTime();
+  return cachedCredential;
+}
+
+/**
+ * 前端直传文件到 OSS
+ *
+ * @param file     待上传文件
+ * @param dir      上传目录(如 "shop-archive/contract")
+ * @param onProgress 上传进度回调 (percent: number) => void
+ * @returns        上传结果(URL、objectKey等)
+ */
+export async function uploadToOss(
+  file: File,
+  dir: string = 'shop-archive',
+  onProgress?: (percent: number) => void
+): Promise<UploadResult> {
+  const credential = await getOssCredential(dir);
+
+  const client = new OSS({
+    region: credential.host.match(/oss-(.+?)\./)?.[1] || 'cn-hangzhou',
+    accessKeyId: credential.accessKeyId,
+    accessKeySecret: credential.accessKeySecret,
+    stsToken: credential.securityToken,
+    bucket: credential.host.match(/\/\/(.+?)\./)?.[1] || '',
+    endpoint: credential.host,
+  });
+
+  // 生成 objectKey:dir/yyyyMMdd/uuid.ext
+  const ext = file.name.includes('.') ? file.name.substring(file.name.lastIndexOf('.')) : '';
+  const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
+  const uuid = Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
+  const objectKey = `${credential.dir}${dateStr}/${uuid}${ext}`;
+
+  const result = await client.put(objectKey, file, {
+    progress: (p: number) => onProgress?.(Math.round(p * 100)),
+  });
+
+  // 拼接完整访问URL
+  const domainPrefix = credential.host.replace('://', '://');
+  const url = `${domainPrefix}/${objectKey}`;
+
+  return {
+    url,
+    objectKey,
+    fileName: file.name,
+    fileSize: file.size,
+    fileExtension: ext.replace('.', ''),
+  };
+}
+```
+
+#### 9.7.3 前端 API 层
+
+**位置**: `haha-admin-web/src/api/shopArchive.ts`
+
+```typescript
+import { http } from '@/utils/http';
+import { uploadToOss } from '@/utils/ossUpload';
+
+// 获取 OSS 上传凭证
+export function getOssUploadCredential(dir: string) {
+  return http.request('get', '/oss/upload-credential', { params: { dir } });
+}
+
+// 前端直传 OSS(封装)
+export { uploadToOss };
+
+// 删除 OSS 文件
+export function deleteOssFile(objectKey: string) {
+  return http.request('delete', '/oss/file', { params: { objectKey } });
+}
+
+// 门店档案 CRUD
+export function getArchiveList(params: any) {
+  return http.request('get', '/shop-archive/list', { params });
+}
+
+export function getArchiveDetail(id: string | number) {
+  return http.request('get', `/shop-archive/${id}`);
+}
+
+export function createArchive(data: any) {
+  return http.request('post', '/shop-archive', { data });
+}
+
+export function updateArchive(id: string | number, data: any) {
+  return http.request('put', `/shop-archive/${id}`, { data });
+}
+
+export function deleteArchive(id: string | number) {
+  return http.request('delete', `/shop-archive/${id}`);
+}
+
+export function getEnumOptions(enumType: string) {
+  return http.request('get', '/shop-archive/enum-options', { params: { enumType } });
+}
+```
+
+#### 9.7.4 前端附件上传组件使用示例
+
+```vue
+<template>
+  <el-upload
+    :http-request="handleUpload"
+    :show-file-list="true"
+    :before-upload="beforeUpload"
+    multiple
+  >
+    <el-button type="primary">上传合同附件</el-button>
+  </el-upload>
+</template>
+
+<script setup lang="tsx">
+import { uploadToOss } from '@/api/shopArchive';
+
+const handleUpload = async (options: any) => {
+  const file = options.file;
+  try {
+    const result = await uploadToOss(file, 'shop-archive/contract', (percent) => {
+      options.onProgress({ percent });
+    });
+
+    // 将上传结果加入附件列表
+    attachmentList.value.push({
+      attachmentType: 'CONTRACT',
+      fileName: result.fileName,
+      fileUrl: result.url,
+      fileSize: result.fileSize,
+      fileExtension: result.fileExtension,
+    });
+
+    options.onSuccess(result);
+  } catch (err) {
+    options.onError(err);
+  }
+};
+
+const beforeUpload = (file: File) => {
+  const maxSize = 10 * 1024 * 1024; // 10MB
+  if (file.size > maxSize) {
+    ElMessage.error('文件大小不能超过 10MB');
+    return false;
+  }
+  const allowedTypes = ['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx'];
+  const ext = file.name.split('.').pop()?.toLowerCase();
+  if (!ext || !allowedTypes.includes(ext)) {
+    ElMessage.error('仅支持 PDF、图片、Word 格式');
+    return false;
+  }
+  return true;
+};
+</script>
+```
+
+### 9.8 附件分组上传
+
+前端附件区域按 5 种类型分组,每种类型独立上传入口:
+
+| 附件类型 | 枚举值 | 上传目录 | 说明 |
+|---------|--------|---------|------|
+| 合同附件 | `CONTRACT` | `shop-archive/contract` | 支持多文件 |
+| 场地租赁协议 | `LEASE_AGREEMENT` | `shop-archive/lease` | 支持多文件 |
+| 电费补充协议 | `ELECTRICITY_AGREEMENT` | `shop-archive/electricity` | 支持多文件 |
+| 保证金收据 | `DEPOSIT_RECEIPT` | `shop-archive/deposit` | 支持多文件 |
+| 解约/补充变更协议 | `TERMINATION_AGREEMENT` | `shop-archive/termination` | 支持多文件 |
+
+---
+
+## 十、菜单与权限配置
+
+### 10.1 权限点
+
+| 权限标识 | 说明 |
+|---------|------|
+| `shop:archive:read` | 查看门店档案 |
+| `shop:archive:create` | 创建门店档案 |
+| `shop:archive:update` | 编辑门店档案 |
+| `shop:archive:delete` | 删除门店档案 |
+
+### 10.2 菜单结构建议
+
+在现有"运营管理 > 门店管理"下新增子菜单:
+
+```
+运营管理
+├── 门店管理(现有)
+├── 门店档案管理(新增)
+│   ├── 查看
+│   ├── 创建
+│   ├── 编辑
+│   └── 删除
+├── 设备管理(现有)
+├── ...
+```
+
+---
+
+## 十一、SQL 迁移脚本
+
+**位置**: `docs/database/create_shop_archive.sql`
+
+包含三张表的完整建表语句(见第二章),以及初始菜单/权限数据插入。
+
+---
+
+## 十二、前端页面设计要点
+
+### 12.1 档案列表页
+
+- 表格列:门店名称、合同编号、合同状态(标签)、经营状态(标签)、计费模式、每月租金、创建时间、操作
+- 筛选条件:门店名称(模糊)、合同编号、合同状态(下拉)、经营状态(下拉)、计费模式(下拉)
+- 操作列:查看、编辑、删除
+
+### 12.2 档案详情/编辑页
+
+采用分步 Tab 或折叠面板形式,按六大模块组织:
+
+```
+┌─────────────────────────────────────────────┐
+│  Tab1: 门店基础信息                           │
+│  Tab2: 固定资产设备                           │
+│  Tab3: 合同核心信息                           │
+│  Tab4: 收款结算 & 开票                        │
+│  Tab5: 合同附件                               │
+└─────────────────────────────────────────────┘
+```
+
+**字段联动规则**:
+- 选择"计费模式"后,动态显示/隐藏电费和提成相关字段
+- 选择"打款方式=对公"时,显示开户行字段
+- 合同状态为"已解约终止"时,显示解约日期和解约备注
+- 固定资产支持动态增删行(表格内编辑)
+- 附件按类型分组展示,每组支持多文件上传
+
+### 12.3 路由配置
+
+在 `haha-admin-web/src/router/modules/operation.ts` 中新增:
+
+```typescript
+{
+  path: "/operation/shop-archive",
+  name: "ShopArchiveList",
+  component: () => import("@/views/shop-archive/index.vue"),
+  meta: {
+    icon: "ri:file-text-line",
+    title: "门店档案管理"
+  }
+}
+```
+
+---
+
+## 十三、开发任务清单
+
+### Phase 1:基础搭建
+
+- [ ] 执行建表 SQL(`t_shop_archive`、`t_shop_fixed_asset`、`t_shop_contract_attachment`)
+- [ ] 新增 12 个枚举类(VenueType、BusinessStatus、ContractStatus、DepositStatus、BillingMode、PaymentCycle、AssetType、AssetStatus、AttachmentType、PaymentMethod、InvoiceType、CommissionBaseType)
+- [ ] 新增 3 个 Entity 类(ShopArchive、ShopFixedAsset、ShopContractAttachment)
+- [ ] 新增 3 个 Mapper 接口 + XML
+- [ ] 新增 DTO 类(ShopArchiveCreateDTO、ShopArchiveUpdateDTO、ShopArchiveQueryDTO、FixedAssetDTO、AttachmentDTO)
+
+### Phase 2:业务逻辑
+
+- [ ] 新增 OssConfig 配置类(haha-common)
+- [ ] 新增 OssService 接口与实现(STS 临时凭证签发 + 文件删除)
+- [ ] 新增 OssUploadController(获取凭证 + 删除文件)
+- [ ] 新增 ShopArchiveService 接口与实现
+- [ ] 新增 ShopArchiveController(7 个 API 接口)
+- [ ] 配置 OSS 相关 application.yml 配置项(AK/SK/STS Role)
+- [ ] 插入菜单和权限数据
+
+### Phase 3:前端开发
+
+- [ ] 安装 `ali-oss` npm 依赖
+- [ ] 新增 `ossUpload.ts` 上传工具类(凭证缓存 + 直传逻辑)
+- [ ] 新增 API 文件 `shopArchive.ts`
+- [ ] 新增门店档案列表页
+- [ ] 新增门店档案详情/编辑页(含 Tab 分组、字段联动、附件分组上传)
+- [ ] 路由配置与菜单对接
+
+### Phase 4:测试与优化
+
+- [ ] 接口联调测试
+- [ ] OSS STS 凭证签发与直传测试
+- [ ] 附件上传/预览/下载/删除测试
+- [ ] 权限校验测试
+- [ ] 字段联动逻辑测试

+ 2 - 2
haha-admin-web/package.json

@@ -61,6 +61,7 @@
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^5.1.12",
     "@wangeditor/editor-for-vue": "^5.1.12",
     "@zxcvbn-ts/core": "^3.0.4",
     "@zxcvbn-ts/core": "^3.0.4",
+    "ali-oss": "^6.23.0",
     "animate.css": "^4.1.1",
     "animate.css": "^4.1.1",
     "axios": "1.14.0",
     "axios": "1.14.0",
     "china-area-data": "^5.0.1",
     "china-area-data": "^5.0.1",
@@ -116,11 +117,11 @@
     "@commitlint/config-conventional": "^20.5.0",
     "@commitlint/config-conventional": "^20.5.0",
     "@commitlint/types": "^20.5.0",
     "@commitlint/types": "^20.5.0",
     "@eslint/js": "^10.0.1",
     "@eslint/js": "^10.0.1",
-
     "@iconify/json": "^2.2.467",
     "@iconify/json": "^2.2.467",
     "@iconify/vue": "4.2.0",
     "@iconify/vue": "4.2.0",
     "@intlify/unplugin-vue-i18n": "^11.1.2",
     "@intlify/unplugin-vue-i18n": "^11.1.2",
     "@tailwindcss/vite": "^4.2.4",
     "@tailwindcss/vite": "^4.2.4",
+    "@types/ali-oss": "^6.23.3",
     "@types/codemirror": "^5.60.17",
     "@types/codemirror": "^5.60.17",
     "@types/dagre": "^0.7.54",
     "@types/dagre": "^0.7.54",
     "@types/intro.js": "^5.1.5",
     "@types/intro.js": "^5.1.5",
@@ -166,7 +167,6 @@
     "vite": "^8.0.10",
     "vite": "^8.0.10",
     "vite-plugin-cdn-import": "^1.0.1",
     "vite-plugin-cdn-import": "^1.0.1",
     "vite-plugin-compression": "^0.5.1",
     "vite-plugin-compression": "^0.5.1",
-
     "vite-plugin-remove-console": "^2.2.0",
     "vite-plugin-remove-console": "^2.2.0",
     "vite-plugin-router-warn": "^2.0.0",
     "vite-plugin-router-warn": "^2.0.0",
     "vite-svg-loader": "^5.1.1",
     "vite-svg-loader": "^5.1.1",

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 369 - 0
haha-admin-web/pnpm-lock.yaml


+ 83 - 0
haha-admin-web/src/api/shopArchive.ts

@@ -0,0 +1,83 @@
+import { http } from "@/utils/http";
+import { uploadToOss, type UploadResult } from "@/utils/ossUpload";
+
+type Result = {
+  code: number;
+  message: string;
+  data?: any;
+};
+
+type ResultTable = {
+  code: number;
+  message: string;
+  data?: {
+    list: Array<any>;
+    total: number;
+    pageSize: number;
+    currentPage: number;
+  };
+};
+
+// ============ OSS 上传相关 ============
+
+/** 获取 OSS 上传凭证 */
+export function getOssUploadCredential(dir: string) {
+  return http.request<Result>("get", "/oss/upload-credential", {
+    params: { dir }
+  });
+}
+
+/** 前端直传 OSS */
+export { uploadToOss };
+export type { UploadResult };
+
+/** 删除 OSS 文件 */
+export function deleteOssFile(objectKey: string) {
+  return http.request<Result>("delete", "/oss/file", {
+    params: { objectKey }
+  });
+}
+
+// ============ 门店档案 CRUD ============
+
+/** 分页查询门店档案列表 */
+export function getArchiveList(params: {
+  page?: number;
+  pageSize?: number;
+  shopName?: string;
+  status?: number;
+}) {
+  return http.request<ResultTable>("get", "/shop-archive/list", { params });
+}
+
+/** 查询门店档案详情 */
+export function getArchiveDetail(id: string | number) {
+  return http.request<Result>("get", `/shop-archive/${id}`);
+}
+
+/** 根据门店ID查询档案 */
+export function getArchiveByShopId(shopId: string | number) {
+  return http.request<Result>("get", `/shop-archive/by-shop/${shopId}`);
+}
+
+/** 创建门店档案 */
+export function createArchive(data: any) {
+  return http.request<Result>("post", "/shop-archive", { data });
+}
+
+/** 更新门店档案 */
+export function updateArchive(id: string | number, data: any) {
+  return http.request<Result>("put", `/shop-archive/${id}`, { data });
+}
+
+/** 删除门店档案 */
+export function deleteArchive(id: string | number) {
+  return http.request<Result>("delete", `/shop-archive/${id}`);
+}
+
+/** 获取枚举选项 */
+export function getEnumOptions(enumType: string) {
+  return http.request<Result>("get", "/shop-archive/enum-options", {
+    params: { enumType }
+  });
+}

+ 20 - 0
haha-admin-web/src/router/modules/operation.ts

@@ -51,6 +51,26 @@ export default {
       meta: {
       meta: {
         title: "ERP店铺管理"
         title: "ERP店铺管理"
       }
       }
+    },
+    {
+      path: "/operation/shop-archive",
+      name: "ShopArchiveList",
+      component: () => import("@/views/shop-archive/index.vue"),
+      meta: {
+        icon: "ri:file-text-line",
+        title: "门店档案管理",
+        keepAlive: true
+      }
+    },
+    {
+      path: "/operation/shop-archive/detail/:id?",
+      name: "ShopArchiveDetail",
+      component: () => import("@/views/shop-archive/detail.vue"),
+      meta: {
+        icon: "ri:file-text-line",
+        title: "门店档案详情",
+        showLink: false
+      }
     }
     }
   ]
   ]
 } satisfies RouteConfigsTable;
 } satisfies RouteConfigsTable;

+ 104 - 0
haha-admin-web/src/utils/ossUpload.ts

@@ -0,0 +1,104 @@
+import OSS from "ali-oss";
+import { http } from "@/utils/http";
+
+interface OssCredential {
+  accessKeyId: string;
+  accessKeySecret: string;
+  securityToken: string;
+  expiration: string;
+  host: string;
+  dir: string;
+}
+
+export interface UploadResult {
+  url: string;
+  objectKey: string;
+  fileName: string;
+  fileSize: number;
+  fileExtension: string;
+}
+
+let cachedCredential: OssCredential | null = null;
+let credentialExpireTime = 0;
+
+/**
+ * 获取 OSS 上传凭证(带缓存,过期前60秒自动刷新)
+ */
+async function getOssCredential(dir: string): Promise<OssCredential> {
+  const now = Date.now();
+  if (cachedCredential && credentialExpireTime - now > 60000) {
+    return cachedCredential;
+  }
+
+  const res = await http.request<{
+    code: number;
+    message: string;
+    data: OssCredential;
+  }>("get", "/oss/upload-credential", { params: { dir } });
+
+  if (res.code !== 200) {
+    throw new Error(res.message || "获取上传凭证失败");
+  }
+
+  cachedCredential = res.data;
+  credentialExpireTime = new Date(res.data.expiration).getTime();
+  return cachedCredential;
+}
+
+/**
+ * 前端直传文件到 OSS
+ *
+ * @param file       待上传文件
+ * @param dir        上传目录(如 "shop-archive/contract")
+ * @param onProgress 上传进度回调 (percent: number) => void
+ * @returns          上传结果(URL、objectKey等)
+ */
+export async function uploadToOss(
+  file: File,
+  dir: string = "shop-archive",
+  onProgress?: (percent: number) => void
+): Promise<UploadResult> {
+  const credential = await getOssCredential(dir);
+
+  // 从 host 中提取 region 和 bucket
+  const hostMatch = credential.host.match(
+    /\/\/(.+?)\.(oss-.+?)\.aliyuncs\.com/
+  );
+  const bucket = hostMatch?.[1] || "";
+  const region = hostMatch?.[2] || "oss-cn-hangzhou";
+
+  const client = new OSS({
+    region,
+    accessKeyId: credential.accessKeyId,
+    accessKeySecret: credential.accessKeySecret,
+    stsToken: credential.securityToken,
+    bucket,
+    endpoint: credential.host
+  });
+
+  // 生成 objectKey:dir/yyyyMMdd/uuid.ext
+  const ext = file.name.includes(".")
+    ? file.name.substring(file.name.lastIndexOf("."))
+    : "";
+  const dateStr = new Date()
+    .toISOString()
+    .slice(0, 10)
+    .replace(/-/g, "");
+  const uuid = Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
+  const objectKey = `${credential.dir}${dateStr}/${uuid}${ext}`;
+
+  await client.put(objectKey, file, {
+    progress: (p: number) => onProgress?.(Math.round(p * 100))
+  });
+
+  // 拼接完整访问URL
+  const url = `${credential.host.replace(/\/$/, "")}/${objectKey}`;
+
+  return {
+    url,
+    objectKey,
+    fileName: file.name,
+    fileSize: file.size,
+    fileExtension: ext.replace(".", "")
+  };
+}

+ 1244 - 0
haha-admin-web/src/views/shop-archive/detail.vue

@@ -0,0 +1,1244 @@
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import { ElMessage } from "element-plus";
+import { ArrowLeft, Plus, Delete, Download, View } from "@element-plus/icons-vue";
+import {
+  getArchiveDetail,
+  createArchive,
+  updateArchive,
+  getEnumOptions,
+  uploadToOss,
+  deleteOssFile
+} from "@/api/shopArchive";
+import type {
+  ShopArchiveForm,
+  FixedAsset,
+  ContractAttachment,
+  EnumOption
+} from "./utils/types";
+import type { UploadRequestOptions, UploadUserFile } from "element-plus";
+
+defineOptions({
+  name: "ShopArchiveDetail"
+});
+
+const route = useRoute();
+const router = useRouter();
+
+const formRef = ref();
+const activeTab = ref("basic");
+const loading = ref(false);
+const saving = ref(false);
+
+// 页面模式
+const isCreate = computed(() => !route.params.id);
+const isView = computed(() => route.query.mode === "view");
+const isEdit = computed(() => !!route.params.id && route.query.mode !== "view");
+const pageTitle = computed(() => {
+  if (isCreate.value) return "新增门店档案";
+  if (isView.value) return "查看门店档案";
+  return "编辑门店档案";
+});
+
+// 表单数据
+const form = reactive<ShopArchiveForm>({
+  shopName: "",
+  shopAddress: "",
+  venueType: "",
+  cooperationMode: "",
+  partyBSalesman: "",
+  partyBSalesmanPhone: "",
+  partyACompany: "",
+  partyAManager: "",
+  partyAManagerPhone: "",
+  businessStatus: "",
+  remark: "",
+  contractNo: "",
+  contractSignDate: "",
+  contractStartDate: "",
+  contractEndDate: "",
+  contractStatus: "",
+  rentStartDate: "",
+  monthlyRent: undefined,
+  rentFreeDays: undefined,
+  rentPayDeadlineDay: undefined,
+  depositAmount: undefined,
+  depositStatus: "",
+  depositPaidAmount: undefined,
+  depositRefundCondition: "",
+  billingMode: "",
+  electricityPrice: undefined,
+  electricityShareRule: "",
+  commissionBaseType: "",
+  commissionBase: undefined,
+  commissionRate: undefined,
+  commissionMinGuarantee: undefined,
+  commissionPeriodMonths: undefined,
+  paymentCycle: "",
+  terminationDate: "",
+  terminationRemark: "",
+  paymentMethod: "",
+  payeeAccountName: "",
+  payeeBankName: "",
+  payeeBankAccount: "",
+  taxpayerId: "",
+  invoiceType: "",
+  invoiceTaxRate: undefined,
+  fixedAssets: [],
+  attachments: []
+});
+
+// 枚举选项
+const contractStatusOptions = ref<EnumOption[]>([]);
+const businessStatusOptions = ref<EnumOption[]>([]);
+const billingModeOptions = ref<EnumOption[]>([]);
+const venueTypeOptions = ref<EnumOption[]>([]);
+const depositStatusOptions = ref<EnumOption[]>([]);
+const commissionBaseTypeOptions = ref<EnumOption[]>([]);
+
+// 静态选项
+const cooperationModeOptions = [
+  { label: "水饮副食", value: "DRINKS_SNACKS" },
+  { label: "盒饭", value: "BOXED_MEAL" }
+];
+
+const paymentMethodOptions = [
+  { label: "对公", value: "CORPORATE" },
+  { label: "对私", value: "PERSONAL" }
+];
+
+const invoiceTypeOptions = [
+  { label: "增值税专票", value: "SPECIAL_VAT" },
+  { label: "增值税普票", value: "GENERAL_VAT" }
+];
+
+const paymentCycleOptions = [
+  { label: "月付", value: "MONTHLY" },
+  { label: "季度付", value: "QUARTERLY" },
+  { label: "半年付", value: "SEMI_ANNUAL" },
+  { label: "年付", value: "ANNUAL" }
+];
+
+const assetTypeOptions = [
+  { label: "自助售卖机", value: "VENDING_MACHINE" },
+  { label: "微波炉", value: "MICROWAVE" }
+];
+
+const assetStatusOptions = [
+  { label: "正常运行", value: "NORMAL" },
+  { label: "故障维修", value: "REPAIR" },
+  { label: "闲置撤机", value: "IDLE" }
+];
+
+// 条件显示
+const showElectricityFields = computed(
+  () => form.billingMode === "ELECTRICITY"
+);
+const showCommissionFields = computed(
+  () => form.billingMode === "COMMISSION"
+);
+const showBankName = computed(
+  () => form.paymentMethod === "CORPORATE"
+);
+const showTerminationFields = computed(
+  () => form.contractStatus === "TERMINATED"
+);
+
+// 固定资产表格列
+const assetColumns = [
+  { label: "资产类型", prop: "assetType", minWidth: 150 },
+  { label: "资产名称", prop: "assetName", minWidth: 150 },
+  { label: "资产编号", prop: "assetCode", minWidth: 150 },
+  { label: "设备状态", prop: "assetStatus", minWidth: 150 },
+  { label: "备注", prop: "remark", minWidth: 200 },
+  { label: "操作", prop: "action", width: 80, fixed: "right" }
+];
+
+// 附件分组
+const attachmentGroups = [
+  { type: "CONTRACT", title: "合同附件" },
+  { type: "LEASE_AGREEMENT", title: "场地租赁协议" },
+  { type: "ELECTRICITY_AGREEMENT", title: "电费补充协议" },
+  { type: "DEPOSIT_RECEIPT", title: "保证金收据" },
+  { type: "TERMINATION_AGREEMENT", title: "解约/补充变更协议" }
+];
+
+// 各分组文件列表
+const fileLists = reactive<Record<string, UploadUserFile[]>>({
+  CONTRACT: [],
+  LEASE_AGREEMENT: [],
+  ELECTRICITY_AGREEMENT: [],
+  DEPOSIT_RECEIPT: [],
+  TERMINATION_AGREEMENT: []
+});
+
+// 文件大小格式化
+function formatFileSize(size: number): string {
+  if (!size) return "0 B";
+  if (size < 1024) return `${size} B`;
+  if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
+  return `${(size / (1024 * 1024)).toFixed(1)} MB`;
+}
+
+// OSS 自定义上传
+async function customUpload(options: UploadRequestOptions) {
+  const file = options.file;
+  const attachmentType = options.data?.attachmentType as string;
+
+  try {
+    options.onProgress({ percent: 0 });
+    const result = await uploadToOss(
+      file,
+      `shop-archive/${attachmentType.toLowerCase()}`,
+      (percent: number) => {
+        options.onProgress({ percent });
+      }
+    );
+
+    const newFile: UploadUserFile = {
+      name: result.fileName,
+      url: result.url,
+      uid: Date.now(),
+      status: "success",
+      size: result.fileSize
+    };
+    (newFile as any).objectKey = result.objectKey;
+    (newFile as any).attachmentType = attachmentType;
+    (newFile as any).fileExtension = result.fileExtension;
+
+    fileLists[attachmentType].push(newFile);
+    options.onSuccess(newFile);
+  } catch (error) {
+    options.onError(error as Error);
+    ElMessage.error(`文件 ${file.name} 上传失败`);
+  }
+}
+
+// 删除附件
+function handleRemove(file: UploadUserFile, attachmentType: string) {
+  const list = fileLists[attachmentType];
+  const index = list.findIndex(f => f.uid === file.uid);
+  if (index > -1) {
+    list.splice(index, 1);
+  }
+  if ((file as any).objectKey) {
+    deleteOssFile((file as any).objectKey).catch(() => {
+      ElMessage.warning("OSS 文件删除失败,但已从列表移除");
+    });
+  }
+}
+
+// 预览附件
+function handlePreview(file: UploadUserFile) {
+  if (file.url) {
+    window.open(file.url, "_blank");
+  }
+}
+
+// 下载附件
+function handleDownload(file: UploadUserFile) {
+  if (file.url) {
+    const link = document.createElement("a");
+    link.href = file.url;
+    link.download = file.name || "下载文件";
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+  }
+}
+
+// 添加固定资产行
+function addAsset() {
+  form.fixedAssets.push({
+    id: `temp_${Date.now()}`,
+    shopArchiveId: "",
+    shopId: "",
+    assetType: "",
+    assetName: "",
+    assetCode: "",
+    assetStatus: "",
+    remark: ""
+  });
+}
+
+// 删除固定资产行
+function removeAsset(index: number) {
+  form.fixedAssets.splice(index, 1);
+}
+
+// 加载枚举选项
+async function loadEnumOptions() {
+  try {
+    const [contractRes, businessRes, billingRes, venueRes, depositRes, commBaseRes] =
+      await Promise.all([
+        getEnumOptions("contractStatus"),
+        getEnumOptions("businessStatus"),
+        getEnumOptions("billingMode"),
+        getEnumOptions("venueType"),
+        getEnumOptions("depositStatus"),
+        getEnumOptions("commissionBaseType")
+      ]);
+    contractStatusOptions.value = contractRes.data || [];
+    businessStatusOptions.value = businessRes.data || [];
+    billingModeOptions.value = billingRes.data || [];
+    venueTypeOptions.value = venueRes.data || [];
+    depositStatusOptions.value = depositRes.data || [];
+    commissionBaseTypeOptions.value = commBaseRes.data || [];
+  } catch (error) {
+    console.error("加载枚举选项失败:", error);
+  }
+}
+
+// 将附件列表转换为提交数据
+function buildAttachments(): ContractAttachment[] {
+  const result: ContractAttachment[] = [];
+  for (const group of attachmentGroups) {
+    const files = fileLists[group.type] || [];
+    files.forEach((file, index) => {
+      result.push({
+        id: (file as any).id || `temp_${file.uid}`,
+        shopArchiveId: "",
+        shopId: "",
+        attachmentType: group.type,
+        fileName: file.name,
+        fileUrl: file.url,
+        fileSize: file.size || 0,
+        fileExtension: (file as any).fileExtension || "",
+        sortOrder: index
+      });
+    });
+  }
+  return result;
+}
+
+// 构建提交数据
+function buildSubmitData() {
+  return {
+    ...form,
+    fixedAssets: form.fixedAssets.filter(a => a.assetType || a.assetName),
+    attachments: buildAttachments()
+  };
+}
+
+// 加载档案详情
+async function loadArchiveDetail() {
+  const id = route.params.id as string;
+  if (!id) return;
+
+  loading.value = true;
+  try {
+    const res = await getArchiveDetail(id);
+    if (res && res.data) {
+      const data = res.data;
+      Object.keys(form).forEach(key => {
+        if (data[key] !== undefined) {
+          (form as any)[key] = data[key];
+        }
+      });
+
+      // 回填固定资产
+      if (data.fixedAssets) {
+        form.fixedAssets = data.fixedAssets;
+      }
+
+      // 回填附件
+      if (data.attachments) {
+        for (const group of attachmentGroups) {
+          const groupFiles = (data.attachments as ContractAttachment[]).filter(
+            (a: ContractAttachment) => a.attachmentType === group.type
+          );
+          fileLists[group.type] = groupFiles.map((a: ContractAttachment) => ({
+            name: a.fileName || "",
+            url: a.fileUrl || "",
+            uid: Number(a.id) || Date.now(),
+            status: "success" as const,
+            size: a.fileSize || 0,
+            objectKey: (a as any).objectKey || "",
+            attachmentType: a.attachmentType,
+            fileExtension: a.fileExtension || "",
+            id: a.id
+          }));
+        }
+      }
+    }
+  } catch (error) {
+    console.error("加载档案详情失败:", error);
+    ElMessage.error("加载档案详情失败");
+  } finally {
+    loading.value = false;
+  }
+}
+
+// 保存
+async function handleSave() {
+  if (!formRef.value) return;
+
+  try {
+    await formRef.value.validate();
+  } catch {
+    ElMessage.warning("请检查表单必填项");
+    return;
+  }
+
+  if (!form.shopName) {
+    ElMessage.warning("请输入门店名称");
+    return;
+  }
+
+  saving.value = true;
+  try {
+    const submitData = buildSubmitData();
+    if (isCreate.value) {
+      const res = await createArchive(submitData);
+      if (res.code === 200 || res.code === 0) {
+        ElMessage.success("创建成功");
+        router.push("/operation/shop-archive");
+      } else {
+        ElMessage.error(res.message || "创建失败");
+      }
+    } else {
+      const res = await updateArchive(route.params.id as string, submitData);
+      if (res.code === 200 || res.code === 0) {
+        ElMessage.success("保存成功");
+        router.push("/operation/shop-archive");
+      } else {
+        ElMessage.error(res.message || "保存失败");
+      }
+    }
+  } catch (error) {
+    console.error("保存失败:", error);
+    ElMessage.error("保存失败");
+  } finally {
+    saving.value = false;
+  }
+}
+
+// 返回
+function goBack() {
+  router.push("/operation/shop-archive");
+}
+
+onMounted(() => {
+  loadEnumOptions();
+  if (route.params.id) {
+    loadArchiveDetail();
+  }
+});
+</script>
+
+<template>
+  <div class="shop-archive-detail" v-loading="loading">
+    <!-- 页面头部 -->
+    <div class="page-header">
+      <el-button :icon="ArrowLeft" @click="goBack">返回列表</el-button>
+      <span class="page-title">{{ pageTitle }}</span>
+    </div>
+
+    <!-- 表单主体 -->
+    <el-form
+      ref="formRef"
+      :model="form"
+      label-width="160px"
+      :disabled="isView"
+      class="detail-form"
+    >
+      <el-tabs v-model="activeTab" type="border-card">
+        <!-- ========== Tab 1: 门店基础信息 ========== -->
+        <el-tab-pane label="门店基础信息" name="basic">
+          <el-row :gutter="24">
+            <el-col :span="8">
+              <el-form-item label="门店名称" required>
+                <el-input
+                  v-model="form.shopName"
+                  placeholder="请输入门店名称"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="门店地址">
+                <el-input
+                  v-model="form.shopAddress"
+                  placeholder="请输入门店地址"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="合作场地类型">
+                <el-select
+                  v-model="form.venueType"
+                  placeholder="请选择合作场地类型"
+                  clearable
+                  class="w-full"
+                >
+                  <el-option
+                    v-for="item in venueTypeOptions"
+                    :key="item.code"
+                    :label="item.description"
+                    :value="item.code"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="合作模式">
+                <el-select
+                  v-model="form.cooperationMode"
+                  placeholder="请选择合作模式"
+                  clearable
+                  multiple
+                  collapse-tags
+                  collapse-tags-tooltip
+                  class="w-full"
+                >
+                  <el-option
+                    v-for="item in cooperationModeOptions"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="乙方业务员姓名">
+                <el-input
+                  v-model="form.partyBSalesman"
+                  placeholder="请输入乙方业务员姓名"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="乙方业务员手机号">
+                <el-input
+                  v-model="form.partyBSalesmanPhone"
+                  placeholder="请输入手机号"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="甲方公司名称">
+                <el-input
+                  v-model="form.partyACompany"
+                  placeholder="请输入甲方公司名称"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="甲方场地管理人员姓名">
+                <el-input
+                  v-model="form.partyAManager"
+                  placeholder="请输入甲方场地管理人员姓名"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="甲方场地管理人员电话">
+                <el-input
+                  v-model="form.partyAManagerPhone"
+                  placeholder="请输入甲方场地管理人员电话"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="门店经营状态">
+                <el-select
+                  v-model="form.businessStatus"
+                  placeholder="请选择门店经营状态"
+                  clearable
+                  class="w-full"
+                >
+                  <el-option
+                    v-for="item in businessStatusOptions"
+                    :key="item.code"
+                    :label="item.description"
+                    :value="item.code"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="16">
+              <el-form-item label="门店备注">
+                <el-input
+                  v-model="form.remark"
+                  type="textarea"
+                  :rows="3"
+                  placeholder="请输入门店备注"
+                />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-tab-pane>
+
+        <!-- ========== Tab 2: 固定资产设备 ========== -->
+        <el-tab-pane label="固定资产设备" name="assets">
+          <el-table :data="form.fixedAssets" border style="width: 100%">
+            <el-table-column label="资产类型" min-width="150">
+              <template #default="{ row }">
+                <el-select
+                  v-model="row.assetType"
+                  placeholder="请选择"
+                  clearable
+                  :disabled="isView"
+                >
+                  <el-option
+                    v-for="item in assetTypeOptions"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+              </template>
+            </el-table-column>
+            <el-table-column label="资产名称" min-width="150">
+              <template #default="{ row }">
+                <el-input
+                  v-model="row.assetName"
+                  placeholder="请输入"
+                  :disabled="isView"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column label="资产编号" min-width="150">
+              <template #default="{ row }">
+                <el-input
+                  v-model="row.assetCode"
+                  placeholder="请输入"
+                  :disabled="isView"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column label="设备状态" min-width="150">
+              <template #default="{ row }">
+                <el-select
+                  v-model="row.assetStatus"
+                  placeholder="请选择"
+                  clearable
+                  :disabled="isView"
+                >
+                  <el-option
+                    v-for="item in assetStatusOptions"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+              </template>
+            </el-table-column>
+            <el-table-column label="备注" min-width="200">
+              <template #default="{ row }">
+                <el-input
+                  v-model="row.remark"
+                  placeholder="请输入"
+                  :disabled="isView"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="80" fixed="right">
+              <template #default="{ $index }">
+                <el-button
+                  type="danger"
+                  link
+                  :icon="Delete"
+                  :disabled="isView"
+                  @click="removeAsset($index)"
+                />
+              </template>
+            </el-table-column>
+          </el-table>
+          <el-button
+            type="primary"
+            plain
+            :icon="Plus"
+            class="mt-4"
+            :disabled="isView"
+            @click="addAsset"
+          >
+            添加设备
+          </el-button>
+        </el-tab-pane>
+
+        <!-- ========== Tab 3: 合同核心信息 ========== -->
+        <el-tab-pane label="合同核心信息" name="contract">
+          <el-row :gutter="24">
+            <el-col :span="8">
+              <el-form-item label="合同编号">
+                <el-input
+                  v-model="form.contractNo"
+                  placeholder="请输入合同编号"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="合同签订日期">
+                <el-date-picker
+                  v-model="form.contractSignDate"
+                  type="date"
+                  placeholder="请选择签订日期"
+                  value-format="YYYY-MM-DD"
+                  class="w-full"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="合同起始日">
+                <el-date-picker
+                  v-model="form.contractStartDate"
+                  type="date"
+                  placeholder="请选择起始日"
+                  value-format="YYYY-MM-DD"
+                  class="w-full"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="合同到期日">
+                <el-date-picker
+                  v-model="form.contractEndDate"
+                  type="date"
+                  placeholder="请选择到期日"
+                  value-format="YYYY-MM-DD"
+                  class="w-full"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="合同状态">
+                <el-select
+                  v-model="form.contractStatus"
+                  placeholder="请选择合同状态"
+                  clearable
+                  class="w-full"
+                >
+                  <el-option
+                    v-for="item in contractStatusOptions"
+                    :key="item.code"
+                    :label="item.description"
+                    :value="item.code"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="租金起始日">
+                <el-date-picker
+                  v-model="form.rentStartDate"
+                  type="date"
+                  placeholder="请选择租金起始日"
+                  value-format="YYYY-MM-DD"
+                  class="w-full"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="每月固定租金金额">
+                <el-input-number
+                  v-model="form.monthlyRent"
+                  :min="0"
+                  :precision="2"
+                  :controls="false"
+                  placeholder="请输入"
+                  class="w-full"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="免租期天数">
+                <el-input-number
+                  v-model="form.rentFreeDays"
+                  :min="0"
+                  :precision="0"
+                  :controls="false"
+                  placeholder="请输入"
+                  class="w-full"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="租金缴纳截止日">
+                <el-input-number
+                  v-model="form.rentPayDeadlineDay"
+                  :min="1"
+                  :max="31"
+                  :precision="0"
+                  :controls="false"
+                  placeholder="每月几号"
+                  class="w-full"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="保证金总额">
+                <el-input-number
+                  v-model="form.depositAmount"
+                  :min="0"
+                  :precision="2"
+                  :controls="false"
+                  placeholder="请输入"
+                  class="w-full"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="保证金缴纳状态">
+                <el-select
+                  v-model="form.depositStatus"
+                  placeholder="请选择"
+                  clearable
+                  class="w-full"
+                >
+                  <el-option
+                    v-for="item in depositStatusOptions"
+                    :key="item.code"
+                    :label="item.description"
+                    :value="item.code"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="保证金实缴金额">
+                <el-input-number
+                  v-model="form.depositPaidAmount"
+                  :min="0"
+                  :precision="2"
+                  :controls="false"
+                  placeholder="请输入"
+                  class="w-full"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="24">
+              <el-form-item label="保证金退还条件">
+                <el-input
+                  v-model="form.depositRefundCondition"
+                  type="textarea"
+                  :rows="2"
+                  placeholder="请输入保证金退还条件"
+                />
+              </el-form-item>
+            </el-col>
+
+            <el-col :span="8">
+              <el-form-item label="合作计费模式">
+                <el-select
+                  v-model="form.billingMode"
+                  placeholder="请选择合作计费模式"
+                  clearable
+                  class="w-full"
+                >
+                  <el-option
+                    v-for="item in billingModeOptions"
+                    :key="item.code"
+                    :label="item.description"
+                    :value="item.code"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+
+            <!-- 电费相关字段 -->
+            <template v-if="showElectricityFields">
+              <el-col :span="8">
+                <el-form-item label="电费结算单价">
+                  <el-input-number
+                    v-model="form.electricityPrice"
+                    :min="0"
+                    :precision="2"
+                    :controls="false"
+                    placeholder="请输入"
+                    class="w-full"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="16">
+                <el-form-item label="电费公摊规则">
+                  <el-input
+                    v-model="form.electricityShareRule"
+                    placeholder="请输入电费公摊规则"
+                  />
+                </el-form-item>
+              </el-col>
+            </template>
+
+            <!-- 提成相关字段 -->
+            <template v-if="showCommissionFields">
+              <el-col :span="8">
+                <el-form-item label="提成基数计算口径">
+                  <el-select
+                    v-model="form.commissionBaseType"
+                    placeholder="请选择"
+                    clearable
+                    class="w-full"
+                  >
+                    <el-option
+                      v-for="item in commissionBaseTypeOptions"
+                      :key="item.code"
+                      :label="item.description"
+                      :value="item.code"
+                    />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="提成基数">
+                  <el-input-number
+                    v-model="form.commissionBase"
+                    :min="0"
+                    :precision="2"
+                    :controls="false"
+                    placeholder="请输入"
+                    class="w-full"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="提成比例(%)">
+                  <el-input-number
+                    v-model="form.commissionRate"
+                    :min="0"
+                    :max="100"
+                    :precision="2"
+                    :controls="false"
+                    placeholder="请输入"
+                    class="w-full"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="最低保底提成金额">
+                  <el-input-number
+                    v-model="form.commissionMinGuarantee"
+                    :min="0"
+                    :precision="2"
+                    :controls="false"
+                    placeholder="请输入"
+                    class="w-full"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="提成账期(月)">
+                  <el-input-number
+                    v-model="form.commissionPeriodMonths"
+                    :min="1"
+                    :precision="0"
+                    :controls="false"
+                    placeholder="请输入"
+                    class="w-full"
+                  />
+                </el-form-item>
+              </el-col>
+            </template>
+
+            <el-col :span="8">
+              <el-form-item label="计费方式">
+                <el-select
+                  v-model="form.paymentCycle"
+                  placeholder="请选择计费方式"
+                  clearable
+                  class="w-full"
+                >
+                  <el-option
+                    v-for="item in paymentCycleOptions"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+
+            <!-- 解约相关字段 -->
+            <template v-if="showTerminationFields">
+              <el-col :span="8">
+                <el-form-item label="解约日期">
+                  <el-date-picker
+                    v-model="form.terminationDate"
+                    type="date"
+                    placeholder="请选择解约日期"
+                    value-format="YYYY-MM-DD"
+                    class="w-full"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="16">
+                <el-form-item label="解约备注">
+                  <el-input
+                    v-model="form.terminationRemark"
+                    type="textarea"
+                    :rows="2"
+                    placeholder="请输入解约备注"
+                  />
+                </el-form-item>
+              </el-col>
+            </template>
+          </el-row>
+        </el-tab-pane>
+
+        <!-- ========== Tab 4: 收款结算 & 开票信息 ========== -->
+        <el-tab-pane label="收款结算 & 开票信息" name="settlement">
+          <el-row :gutter="24">
+            <el-col :span="8">
+              <el-form-item label="打款方式">
+                <el-select
+                  v-model="form.paymentMethod"
+                  placeholder="请选择打款方式"
+                  clearable
+                  class="w-full"
+                >
+                  <el-option
+                    v-for="item in paymentMethodOptions"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="甲方收款账户名称">
+                <el-input
+                  v-model="form.payeeAccountName"
+                  placeholder="请输入甲方收款账户名称"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8" v-if="showBankName">
+              <el-form-item label="收款开户行">
+                <el-input
+                  v-model="form.payeeBankName"
+                  placeholder="请输入收款开户行"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="银行账号">
+                <el-input
+                  v-model="form.payeeBankAccount"
+                  placeholder="请输入银行账号"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="纳税人识别号">
+                <el-input
+                  v-model="form.taxpayerId"
+                  placeholder="请输入纳税人识别号"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="发票类型">
+                <el-select
+                  v-model="form.invoiceType"
+                  placeholder="请选择发票类型"
+                  clearable
+                  class="w-full"
+                >
+                  <el-option
+                    v-for="item in invoiceTypeOptions"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="开票税率(%)">
+                <el-input-number
+                  v-model="form.invoiceTaxRate"
+                  :min="0"
+                  :max="100"
+                  :precision="2"
+                  :controls="false"
+                  placeholder="请输入"
+                  class="w-full"
+                />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-tab-pane>
+
+        <!-- ========== Tab 5: 合同附件 ========== -->
+        <el-tab-pane label="合同附件" name="attachments">
+          <div
+            v-for="group in attachmentGroups"
+            :key="group.type"
+            class="attachment-group"
+          >
+            <div class="attachment-group-title">{{ group.title }}</div>
+            <el-upload
+              :file-list="fileLists[group.type]"
+              :http-request="customUpload"
+              :on-remove="file => handleRemove(file, group.type)"
+              :on-preview="handlePreview"
+              :before-upload="file => { if (file.size > 50 * 1024 * 1024) { ElMessage.error('文件大小不能超过50MB'); return false; } return true; }"
+              :data="{ attachmentType: group.type }"
+              :disabled="isView"
+              multiple
+              :auto-upload="true"
+            >
+              <el-button
+                type="primary"
+                plain
+                :icon="Plus"
+                :disabled="isView"
+              >
+                上传文件
+              </el-button>
+              <template #tip>
+                <div class="el-upload__tip">
+                  支持上传任意格式文件,单个文件不超过 50MB
+                </div>
+              </template>
+            </el-upload>
+            <!-- 已上传文件列表(查看模式) -->
+            <div
+              v-if="fileLists[group.type]?.length && isView"
+              class="file-list-view"
+            >
+              <div
+                v-for="file in fileLists[group.type]"
+                :key="file.uid"
+                class="file-item"
+              >
+                <span class="file-name">{{ file.name }}</span>
+                <span class="file-size">
+                  {{ formatFileSize(file.size || 0) }}
+                </span>
+                <span class="file-actions">
+                  <el-button
+                    type="primary"
+                    link
+                    :icon="View"
+                    @click="handlePreview(file)"
+                  >
+                    预览
+                  </el-button>
+                  <el-button
+                    type="primary"
+                    link
+                    :icon="Download"
+                    @click="handleDownload(file)"
+                  >
+                    下载
+                  </el-button>
+                </span>
+              </div>
+            </div>
+          </div>
+        </el-tab-pane>
+      </el-tabs>
+    </el-form>
+
+    <!-- 底部操作按钮 -->
+    <div class="page-footer" v-if="!isView">
+      <el-button @click="goBack">取消</el-button>
+      <el-button type="primary" :loading="saving" @click="handleSave">
+        保存
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.shop-archive-detail {
+  padding: 16px;
+}
+
+.page-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 16px;
+
+  .page-title {
+    margin-left: 12px;
+    font-size: 18px;
+    font-weight: 600;
+    color: var(--el-text-color-primary);
+  }
+}
+
+.detail-form {
+  :deep(.el-tabs__content) {
+    padding: 24px 16px;
+  }
+
+  :deep(.el-form-item) {
+    margin-bottom: 18px;
+  }
+
+  :deep(.el-input-number) {
+    width: 100%;
+  }
+}
+
+.attachment-group {
+  margin-bottom: 24px;
+  padding-bottom: 16px;
+  border-bottom: 1px solid var(--el-border-color-lighter);
+
+  &:last-child {
+    margin-bottom: 0;
+    padding-bottom: 0;
+    border-bottom: none;
+  }
+
+  .attachment-group-title {
+    font-size: 15px;
+    font-weight: 600;
+    color: var(--el-text-color-primary);
+    margin-bottom: 12px;
+  }
+}
+
+.file-list-view {
+  margin-top: 12px;
+
+  .file-item {
+    display: flex;
+    align-items: center;
+    padding: 8px 12px;
+    border: 1px solid var(--el-border-color-lighter);
+    border-radius: 4px;
+    margin-bottom: 8px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    .file-name {
+      flex: 1;
+      font-size: 14px;
+      color: var(--el-text-color-primary);
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    .file-size {
+      margin: 0 16px;
+      font-size: 12px;
+      color: var(--el-text-color-secondary);
+      white-space: nowrap;
+    }
+
+    .file-actions {
+      display: flex;
+      gap: 8px;
+      white-space: nowrap;
+    }
+  }
+}
+
+.page-footer {
+  display: flex;
+  justify-content: center;
+  gap: 16px;
+  padding: 24px 0;
+  margin-top: 16px;
+  border-top: 1px solid var(--el-border-color-lighter);
+}
+</style>

+ 212 - 0
haha-admin-web/src/views/shop-archive/index.vue

@@ -0,0 +1,212 @@
+<script setup lang="ts">
+import { ref } from "vue";
+import { useShopArchive } from "./utils/hook";
+import { PureTableBar } from "@/components/RePureTableBar";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+
+import Delete from "~icons/ep/delete";
+import EditPen from "~icons/ep/edit-pen";
+import Refresh from "~icons/ep/refresh";
+import AddFill from "~icons/ri/add-circle-line";
+import View from "~icons/ep/view";
+
+defineOptions({
+  name: "ShopArchiveManage"
+});
+
+const formRef = ref();
+const tableRef = ref();
+
+const {
+  form,
+  loading,
+  columns,
+  dataList,
+  pagination,
+  contractStatusOptions,
+  businessStatusOptions,
+  billingModeOptions,
+  onSearch,
+  resetForm,
+  handleAdd,
+  handleUpdate,
+  handleView,
+  handleDelete,
+  handleSizeChange,
+  handleCurrentChange
+} = useShopArchive(tableRef);
+</script>
+
+<template>
+  <div class="main">
+    <el-form
+      ref="formRef"
+      :inline="true"
+      :model="form"
+      class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
+    >
+      <el-form-item label="门店名称:" prop="shopName">
+        <el-input
+          v-model="form.shopName"
+          placeholder="请输入门店名称"
+          clearable
+          class="w-[180px]!"
+        />
+      </el-form-item>
+      <el-form-item label="合同编号:" prop="contractNo">
+        <el-input
+          v-model="form.contractNo"
+          placeholder="请输入合同编号"
+          clearable
+          class="w-[180px]!"
+        />
+      </el-form-item>
+      <el-form-item label="合同状态:" prop="contractStatus">
+        <el-select
+          v-model="form.contractStatus"
+          placeholder="请选择合同状态"
+          clearable
+          class="w-[160px]!"
+        >
+          <el-option
+            v-for="item in contractStatusOptions"
+            :key="item.code"
+            :label="item.description"
+            :value="item.code"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="经营状态:" prop="businessStatus">
+        <el-select
+          v-model="form.businessStatus"
+          placeholder="请选择经营状态"
+          clearable
+          class="w-[160px]!"
+        >
+          <el-option
+            v-for="item in businessStatusOptions"
+            :key="item.code"
+            :label="item.description"
+            :value="item.code"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="计费模式:" prop="billingMode">
+        <el-select
+          v-model="form.billingMode"
+          placeholder="请选择计费模式"
+          clearable
+          class="w-[160px]!"
+        >
+          <el-option
+            v-for="item in billingModeOptions"
+            :key="item.code"
+            :label="item.description"
+            :value="item.code"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon('ri/search-line')"
+          :loading="loading"
+          @click="onSearch"
+        >
+          搜索
+        </el-button>
+        <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <PureTableBar
+      title="门店档案管理"
+      :columns="columns"
+      @refresh="onSearch"
+    >
+      <template #buttons>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon(AddFill)"
+          @click="handleAdd"
+        >
+          新增档案
+        </el-button>
+      </template>
+      <template v-slot="{ size, dynamicColumns }">
+        <pure-table
+          ref="tableRef"
+          row-key="id"
+          adaptive
+          :adaptiveConfig="{ offsetBottom: 108 }"
+          align-whole="center"
+          table-layout="auto"
+          :loading="loading"
+          :size="size"
+          :data="dataList"
+          :columns="dynamicColumns"
+          :pagination="{ ...pagination, size }"
+          :header-cell-style="{
+            background: 'var(--el-fill-color-light)',
+            color: 'var(--el-text-color-primary)'
+          }"
+          @page-size-change="handleSizeChange"
+          @page-current-change="handleCurrentChange"
+        >
+          <template #operation="{ row }">
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(View)"
+              @click="handleView(row)"
+            >
+              查看
+            </el-button>
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(EditPen)"
+              @click="handleUpdate(row)"
+            >
+              编辑
+            </el-button>
+            <el-popconfirm
+              :title="`是否确认删除门店档案 ${row.shopName}?`"
+              @confirm="handleDelete(row)"
+            >
+              <template #reference>
+                <el-button
+                  class="reset-margin"
+                  link
+                  type="danger"
+                  :size="size"
+                  :icon="useRenderIcon(Delete)"
+                >
+                  删除
+                </el-button>
+              </template>
+            </el-popconfirm>
+          </template>
+        </pure-table>
+      </template>
+    </PureTableBar>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.main-content {
+  margin: 24px 24px 0 !important;
+}
+
+.search-form {
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+  }
+}
+</style>

+ 245 - 0
haha-admin-web/src/views/shop-archive/utils/hook.tsx

@@ -0,0 +1,245 @@
+import dayjs from "dayjs";
+import { message } from "@/utils/message";
+import { useRouter } from "vue-router";
+import type { PaginationProps } from "@pureadmin/table";
+import {
+  getArchiveList,
+  deleteArchive,
+  getEnumOptions
+} from "@/api/shopArchive";
+import { type Ref, ref, reactive, onMounted } from "vue";
+import { ElMessageBox } from "element-plus";
+import type { ShopArchive, ShopArchiveQuery, EnumOption } from "./types";
+import {
+  initPagination,
+  handlePageSizeChange,
+  handleCurrentPageChange,
+  resetPagination
+} from "@/utils/paginationHelper";
+
+export function useShopArchive(tableRef: Ref) {
+  const router = useRouter();
+  const form = reactive<ShopArchiveQuery>({
+    shopName: "",
+    contractNo: "",
+    contractStatus: "",
+    businessStatus: "",
+    billingMode: ""
+  });
+  const formRef = ref();
+  const dataList = ref([]);
+  const loading = ref(true);
+  const pagination = reactive<PaginationProps>(initPagination());
+
+  // 枚举选项
+  const contractStatusOptions = ref<EnumOption[]>([]);
+  const businessStatusOptions = ref<EnumOption[]>([]);
+  const billingModeOptions = ref<EnumOption[]>([]);
+
+  const columns: TableColumnList = [
+    {
+      label: "门店名称",
+      prop: "shopName",
+      minWidth: 150
+    },
+    {
+      label: "合同编号",
+      prop: "contractNo",
+      minWidth: 120,
+      formatter: ({ contractNo }) => contractNo || "-"
+    },
+    {
+      label: "合同状态",
+      prop: "contractStatus",
+      minWidth: 100,
+      cellRenderer: ({ row }) => {
+        const statusMap: Record<string, { label: string; type: string }> = {
+          NORMAL: { label: "正常", type: "success" },
+          EXPIRING: { label: "即将到期", type: "warning" },
+          EXPIRED: { label: "已过期", type: "info" },
+          TERMINATED: { label: "已终止", type: "danger" }
+        };
+        const status = statusMap[row.contractStatus] || {
+          label: row.contractStatusLabel || "未知",
+          type: "info"
+        };
+        return <el-tag type={status.type as any}>{status.label}</el-tag>;
+      }
+    },
+    {
+      label: "经营状态",
+      prop: "businessStatus",
+      minWidth: 100,
+      cellRenderer: ({ row }) => {
+        const statusMap: Record<string, { label: string; type: string }> = {
+          NORMAL: { label: "正常", type: "success" },
+          SUSPENDED: { label: "暂停", type: "warning" },
+          WITHDRAWN: { label: "已撤场", type: "danger" }
+        };
+        const status = statusMap[row.businessStatus] || {
+          label: row.businessStatusLabel || "未知",
+          type: "info"
+        };
+        return <el-tag type={status.type as any}>{status.label}</el-tag>;
+      }
+    },
+    {
+      label: "计费模式",
+      prop: "billingMode",
+      minWidth: 100,
+      formatter: ({ billingMode }) => {
+        const modeMap: Record<string, string> = {
+          ELECTRICITY: "电费",
+          COMMISSION: "提成",
+          RENT: "租金"
+        };
+        return modeMap[billingMode] || billingMode || "-";
+      }
+    },
+    {
+      label: "每月租金",
+      prop: "monthlyRent",
+      minWidth: 100,
+      formatter: ({ monthlyRent }) =>
+        monthlyRent ? `¥${monthlyRent.toFixed(2)}` : "-"
+    },
+    {
+      label: "创建时间",
+      prop: "createTime",
+      minWidth: 160,
+      formatter: ({ createTime }) =>
+        createTime ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss") : "-"
+    },
+    {
+      label: "操作",
+      fixed: "right",
+      width: 200,
+      slot: "operation"
+    }
+  ];
+
+  // 加载枚举选项
+  async function loadEnumOptions() {
+    try {
+      const [contractRes, businessRes, billingRes] = await Promise.all([
+        getEnumOptions("contractStatus"),
+        getEnumOptions("businessStatus"),
+        getEnumOptions("billingMode")
+      ]);
+      contractStatusOptions.value = contractRes.data || [];
+      businessStatusOptions.value = businessRes.data || [];
+      billingModeOptions.value = billingRes.data || [];
+    } catch (error) {
+      console.error("加载枚举选项失败:", error);
+    }
+  }
+
+  // 搜索
+  async function onSearch() {
+    loading.value = true;
+    try {
+      const searchParams: any = {
+        page: pagination.currentPage,
+        pageSize: pagination.pageSize
+      };
+
+      if (form.shopName) searchParams.shopName = form.shopName;
+      if (form.contractNo) searchParams.contractNo = form.contractNo;
+      if (form.contractStatus) searchParams.contractStatus = form.contractStatus;
+      if (form.businessStatus) searchParams.businessStatus = form.businessStatus;
+      if (form.billingMode) searchParams.billingMode = form.billingMode;
+
+      const { data } = await getArchiveList(searchParams);
+      dataList.value = data.list || [];
+      pagination.total = Number(data.total) || 0;
+    } catch (error) {
+      console.error("获取门店档案列表失败:", error);
+      dataList.value = [];
+      pagination.total = 0;
+    } finally {
+      setTimeout(() => {
+        loading.value = false;
+      }, 300);
+    }
+  }
+
+  // 重置表单
+  const resetForm = formEl => {
+    if (!formEl) return;
+    formEl.resetFields();
+    resetPagination(pagination, onSearch);
+  };
+
+  // 分页
+  function handleSizeChange(val: number) {
+    handlePageSizeChange(val, pagination, onSearch);
+  }
+
+  function handleCurrentChange(val: number) {
+    handleCurrentPageChange(val, pagination, onSearch);
+  }
+
+  // 新增
+  function handleAdd() {
+    router.push("/operation/shop-archive/create");
+  }
+
+  // 编辑
+  function handleUpdate(row: ShopArchive) {
+    router.push(`/operation/shop-archive/edit/${row.id}`);
+  }
+
+  // 查看
+  function handleView(row: ShopArchive) {
+    router.push(`/operation/shop-archive/detail/${row.id}`);
+  }
+
+  // 删除
+  async function handleDelete(row: ShopArchive) {
+    try {
+      await ElMessageBox.confirm(
+        `确认要删除门店档案 ${row.shopName} 吗?`,
+        "系统提示",
+        {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }
+      );
+
+      const res = await deleteArchive(row.id);
+      if (res.code === 0 || res.code === 200) {
+        message(`已成功删除门店档案 ${row.shopName}`, { type: "success" });
+        onSearch();
+      } else {
+        message(res.message || "删除失败", { type: "error" });
+      }
+    } catch (error) {
+      // 用户取消或请求失败
+    }
+  }
+
+  onMounted(() => {
+    loadEnumOptions();
+    onSearch();
+  });
+
+  return {
+    form,
+    loading,
+    columns,
+    dataList,
+    pagination,
+    contractStatusOptions,
+    businessStatusOptions,
+    billingModeOptions,
+    onSearch,
+    resetForm,
+    handleAdd,
+    handleUpdate,
+    handleView,
+    handleDelete,
+    handleSizeChange,
+    handleCurrentChange
+  };
+}

+ 161 - 0
haha-admin-web/src/views/shop-archive/utils/types.ts

@@ -0,0 +1,161 @@
+// 门店档案信息类型
+export interface ShopArchive {
+  id: string;
+  shopId: string;
+  shopName: string;
+  shopAddress?: string;
+  venueType?: string;
+  cooperationMode?: string;
+  partyBSalesman?: string;
+  partyBSalesmanPhone?: string;
+  partyACompany?: string;
+  partyAManager?: string;
+  partyAManagerPhone?: string;
+  businessStatus?: string;
+  remark?: string;
+  contractNo?: string;
+  contractSignDate?: string;
+  contractStartDate?: string;
+  contractEndDate?: string;
+  contractStatus?: string;
+  rentStartDate?: string;
+  monthlyRent?: number;
+  rentFreeDays?: number;
+  rentPayDeadlineDay?: number;
+  depositAmount?: number;
+  depositStatus?: string;
+  depositPaidAmount?: number;
+  depositRefundCondition?: string;
+  billingMode?: string;
+  electricityPrice?: number;
+  electricityShareRule?: string;
+  commissionBaseType?: string;
+  commissionBase?: number;
+  commissionRate?: number;
+  commissionMinGuarantee?: number;
+  commissionPeriodMonths?: number;
+  paymentCycle?: string;
+  terminationDate?: string;
+  terminationRemark?: string;
+  paymentMethod?: string;
+  payeeAccountName?: string;
+  payeeBankName?: string;
+  payeeBankAccount?: string;
+  taxpayerId?: string;
+  invoiceType?: string;
+  invoiceTaxRate?: number;
+  creatorId?: string;
+  creatorName?: string;
+  createTime?: string;
+  editorId?: string;
+  editorName?: string;
+  updateTime?: string;
+  fixedAssets?: FixedAsset[];
+  attachments?: ContractAttachment[];
+  shopCode?: string;
+  contractStatusLabel?: string;
+  businessStatusLabel?: string;
+}
+
+// 固定资产类型
+export interface FixedAsset {
+  id: string;
+  shopArchiveId: string;
+  shopId: string;
+  assetType?: string;
+  assetName?: string;
+  assetCode?: string;
+  assetStatus?: string;
+  remark?: string;
+  creatorId?: string;
+  creatorName?: string;
+  createTime?: string;
+  editorId?: string;
+  editorName?: string;
+  updateTime?: string;
+  assetTypeLabel?: string;
+  assetStatusLabel?: string;
+}
+
+// 合同附件类型
+export interface ContractAttachment {
+  id: string;
+  shopArchiveId: string;
+  shopId: string;
+  attachmentType?: string;
+  fileName?: string;
+  fileUrl?: string;
+  fileSize?: number;
+  fileExtension?: string;
+  sortOrder?: number;
+  creatorId?: string;
+  creatorName?: string;
+  createTime?: string;
+  attachmentTypeLabel?: string;
+}
+
+// 搜索表单类型
+export interface ShopArchiveQuery {
+  shopId?: string;
+  shopName?: string;
+  contractNo?: string;
+  contractStatus?: string;
+  businessStatus?: string;
+  billingMode?: string;
+  venueType?: string;
+}
+
+// 表单类型(创建/更新)
+export interface ShopArchiveForm {
+  shopId?: string;
+  shopName: string;
+  shopAddress?: string;
+  venueType?: string;
+  cooperationMode?: string;
+  partyBSalesman?: string;
+  partyBSalesmanPhone?: string;
+  partyACompany?: string;
+  partyAManager?: string;
+  partyAManagerPhone?: string;
+  businessStatus?: string;
+  remark?: string;
+  contractNo?: string;
+  contractSignDate?: string;
+  contractStartDate?: string;
+  contractEndDate?: string;
+  contractStatus?: string;
+  rentStartDate?: string;
+  monthlyRent?: number;
+  rentFreeDays?: number;
+  rentPayDeadlineDay?: number;
+  depositAmount?: number;
+  depositStatus?: string;
+  depositPaidAmount?: number;
+  depositRefundCondition?: string;
+  billingMode?: string;
+  electricityPrice?: number;
+  electricityShareRule?: string;
+  commissionBaseType?: string;
+  commissionBase?: number;
+  commissionRate?: number;
+  commissionMinGuarantee?: number;
+  commissionPeriodMonths?: number;
+  paymentCycle?: string;
+  terminationDate?: string;
+  terminationRemark?: string;
+  paymentMethod?: string;
+  payeeAccountName?: string;
+  payeeBankName?: string;
+  payeeBankAccount?: string;
+  taxpayerId?: string;
+  invoiceType?: string;
+  invoiceTaxRate?: number;
+  fixedAssets?: FixedAsset[];
+  attachments?: ContractAttachment[];
+}
+
+// 枚举选项类型
+export interface EnumOption {
+  code: string;
+  description: string;
+}

+ 49 - 0
haha-admin/src/main/java/com/haha/admin/controller/OssUploadController.java

@@ -0,0 +1,49 @@
+package com.haha.admin.controller;
+
+import com.haha.admin.annotation.RequirePermission;
+import com.haha.common.vo.Result;
+import com.haha.service.OssService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * OSS 上传控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/oss")
+@RequiredArgsConstructor
+public class OssUploadController {
+
+    private final OssService ossService;
+
+    /**
+     * 获取前端直传 OSS 的临时凭证
+     *
+     * @param dir 上传目录前缀,默认为 "shop-archive"
+     * @return 包含 STS 临时凭证的 Map
+     */
+    @RequirePermission("shop:archive:create")
+    @GetMapping("/upload-credential")
+    public Result<Map<String, String>> getUploadCredential(
+            @RequestParam(value = "dir", defaultValue = "shop-archive") String dir) {
+        Map<String, String> credential = ossService.getUploadCredential(dir);
+        return Result.success("获取凭证成功", credential);
+    }
+
+    /**
+     * 删除 OSS 文件
+     *
+     * @param objectKey OSS 对象键
+     * @return 删除结果
+     */
+    @RequirePermission("shop:archive:delete")
+    @DeleteMapping("/file")
+    public Result<Void> deleteFile(@RequestParam String objectKey) {
+        ossService.deleteByObjectKey(objectKey);
+        return Result.success("删除成功", null);
+    }
+}

+ 127 - 0
haha-admin/src/main/java/com/haha/admin/controller/ShopArchiveController.java

@@ -0,0 +1,127 @@
+package com.haha.admin.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.haha.admin.annotation.RequirePermission;
+import com.haha.common.annotation.Log;
+import com.haha.common.enums.OperationType;
+import com.haha.common.vo.PageResult;
+import com.haha.common.vo.Result;
+import com.haha.entity.ShopArchive;
+import com.haha.entity.dto.ShopArchiveCreateDTO;
+import com.haha.entity.dto.ShopArchiveQueryDTO;
+import com.haha.entity.dto.ShopArchiveUpdateDTO;
+import com.haha.service.ShopArchiveService;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 门店档案管理控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/shop-archive")
+@RequiredArgsConstructor
+public class ShopArchiveController {
+
+    private final ShopArchiveService shopArchiveService;
+
+    /**
+     * 分页查询门店档案列表
+     * @param queryDTO 查询参数
+     * @return 门店档案列表
+     */
+    @RequirePermission("shop:archive:read")
+    @GetMapping("/list")
+    public Result<PageResult<ShopArchive>> list(ShopArchiveQueryDTO queryDTO) {
+        queryDTO.validate();
+        IPage<ShopArchive> archivePage = shopArchiveService.getPage(queryDTO);
+        return Result.success("查询成功", PageResult.of(archivePage));
+    }
+
+    /**
+     * 查询门店档案详情(含固定资产、附件)
+     * @param id 档案ID
+     * @return 档案详情
+     */
+    @RequirePermission("shop:archive:read")
+    @GetMapping("/{id}")
+    public Result<ShopArchive> getById(@PathVariable Long id) {
+        ShopArchive archive = shopArchiveService.getDetailById(id);
+        if (archive == null) {
+            return Result.error(404, "门店档案不存在");
+        }
+        return Result.success("查询成功", archive);
+    }
+
+    /**
+     * 根据门店ID查询档案
+     * @param shopId 门店ID
+     * @return 门店档案
+     */
+    @RequirePermission("shop:archive:read")
+    @GetMapping("/by-shop/{shopId}")
+    public Result<ShopArchive> getByShopId(@PathVariable Long shopId) {
+        ShopArchive archive = shopArchiveService.getByShopId(shopId);
+        if (archive == null) {
+            return Result.error(404, "该门店尚未创建档案");
+        }
+        return Result.success("查询成功", archive);
+    }
+
+    /**
+     * 创建门店档案
+     * @param dto 创建参数
+     * @return 创建后的档案
+     */
+    @RequirePermission("shop:archive:create")
+    @Log(module = "门店档案管理", operation = OperationType.INSERT, summary = "创建门店档案")
+    @PostMapping
+    public Result<ShopArchive> create(@RequestBody @Valid ShopArchiveCreateDTO dto) {
+        ShopArchive archive = shopArchiveService.create(dto);
+        return Result.success("创建成功", archive);
+    }
+
+    /**
+     * 更新门店档案
+     * @param id 档案ID
+     * @param dto 更新参数
+     * @return 更新后的档案
+     */
+    @RequirePermission("shop:archive:update")
+    @Log(module = "门店档案管理", operation = OperationType.UPDATE, summary = "更新门店档案")
+    @PutMapping("/{id}")
+    public Result<ShopArchive> update(@PathVariable Long id, @RequestBody @Valid ShopArchiveUpdateDTO dto) {
+        dto.setId(id);
+        ShopArchive archive = shopArchiveService.update(dto);
+        return Result.success("更新成功", archive);
+    }
+
+    /**
+     * 删除门店档案
+     * @param id 档案ID
+     * @return 操作结果
+     */
+    @RequirePermission("shop:archive:delete")
+    @Log(module = "门店档案管理", operation = OperationType.DELETE, summary = "删除门店档案")
+    @DeleteMapping("/{id}")
+    public Result<String> delete(@PathVariable Long id) {
+        shopArchiveService.delete(id);
+        return Result.success("删除成功");
+    }
+
+    /**
+     * 获取枚举选项(供前端下拉框使用)
+     * @param enumType 枚举类型:venueType / businessStatus / contractStatus / depositStatus / billingMode / paymentCycle / assetType / assetStatus / attachmentType / paymentMethod / invoiceType / commissionBaseType
+     * @return 枚举选项列表
+     */
+    @RequirePermission("shop:archive:read")
+    @GetMapping("/enum-options")
+    public Result<List<Map<String, String>>> getEnumOptions(@RequestParam String enumType) {
+        return Result.success("查询成功", shopArchiveService.getEnumOptions(enumType));
+    }
+}

+ 13 - 0
haha-admin/src/main/resources/application.yml

@@ -126,6 +126,19 @@ qdb:
   client-secret: zfmR7PEBUA3lpRVQjTn0KxlUmVnWo2OP
   client-secret: zfmR7PEBUA3lpRVQjTn0KxlUmVnWo2OP
   api-url: https://api.7debao.com/router/api
   api-url: https://api.7debao.com/router/api
 
 
+# 阿里云 OSS 配置
+aliyun:
+  oss:
+    endpoint: oss-cn-hangzhou.aliyuncs.com
+    access-key-id: ${OSS_ACCESS_KEY_ID:your-access-key-id}
+    access-key-secret: ${OSS_ACCESS_KEY_SECRET:your-access-key-secret}
+    bucket-name: haha-shop-archive
+    domain-prefix: https://haha-shop-archive.oss-cn-hangzhou.aliyuncs.com
+    sts:
+      role-arn: acs:ram::your-account-id:role/haha-oss-upload-role
+      role-session-name: haha-shop-archive-upload
+      duration-seconds: 900
+
 # 设备离线预警配置
 # 设备离线预警配置
 device:
 device:
   alert:
   alert:

+ 31 - 0
haha-common/src/main/java/com/haha/common/config/OssConfig.java

@@ -0,0 +1,31 @@
+package com.haha.common.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 阿里云 OSS 配置
+ */
+@Data
+@Configuration(proxyBeanMethods = false)
+@ConfigurationProperties(prefix = "aliyun.oss")
+public class OssConfig {
+
+    private String endpoint;
+    private String accessKeyId;
+    private String accessKeySecret;
+    private String bucketName;
+    private String domainPrefix;
+    private StsConfig sts = new StsConfig();
+
+    /**
+     * STS 临时凭证配置
+     */
+    @Data
+    public static class StsConfig {
+        private String roleArn;
+        private String roleSessionName;
+        private int durationSeconds = 900;
+    }
+}

+ 41 - 0
haha-common/src/main/java/com/haha/common/enums/AssetStatus.java

@@ -0,0 +1,41 @@
+package com.haha.common.enums;
+
+public enum AssetStatus {
+
+    NORMAL("NORMAL", "正常运行"),
+    REPAIR("REPAIR", "故障维修"),
+    IDLE("IDLE", "闲置撤机");
+
+    private final String code;
+    private final String description;
+
+    AssetStatus(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public static AssetStatus fromCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        for (AssetStatus assetStatus : values()) {
+            if (assetStatus.code.equals(code)) {
+                return assetStatus;
+            }
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        AssetStatus assetStatus = fromCode(code);
+        return assetStatus != null ? assetStatus.description : "未知";
+    }
+}

+ 40 - 0
haha-common/src/main/java/com/haha/common/enums/AssetType.java

@@ -0,0 +1,40 @@
+package com.haha.common.enums;
+
+public enum AssetType {
+
+    VENDING_MACHINE("VENDING_MACHINE", "自助售卖机"),
+    MICROWAVE("MICROWAVE", "微波炉");
+
+    private final String code;
+    private final String description;
+
+    AssetType(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public static AssetType fromCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        for (AssetType assetType : values()) {
+            if (assetType.code.equals(code)) {
+                return assetType;
+            }
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        AssetType assetType = fromCode(code);
+        return assetType != null ? assetType.description : "未知";
+    }
+}

+ 43 - 0
haha-common/src/main/java/com/haha/common/enums/AttachmentType.java

@@ -0,0 +1,43 @@
+package com.haha.common.enums;
+
+public enum AttachmentType {
+
+    CONTRACT("CONTRACT", "合同附件"),
+    LEASE_AGREEMENT("LEASE_AGREEMENT", "场地租赁协议"),
+    ELECTRICITY_AGREEMENT("ELECTRICITY_AGREEMENT", "电费补充协议"),
+    DEPOSIT_RECEIPT("DEPOSIT_RECEIPT", "保证金收据"),
+    TERMINATION_AGREEMENT("TERMINATION_AGREEMENT", "解约/补充变更协议");
+
+    private final String code;
+    private final String description;
+
+    AttachmentType(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public static AttachmentType fromCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        for (AttachmentType attachmentType : values()) {
+            if (attachmentType.code.equals(code)) {
+                return attachmentType;
+            }
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        AttachmentType attachmentType = fromCode(code);
+        return attachmentType != null ? attachmentType.description : "未知";
+    }
+}

+ 43 - 0
haha-common/src/main/java/com/haha/common/enums/BillingMode.java

@@ -0,0 +1,43 @@
+package com.haha.common.enums;
+
+public enum BillingMode {
+
+    RENT_ONLY("RENT_ONLY", "纯租金"),
+    RENT_ELECTRIC("RENT_ELECTRIC", "租金+电费"),
+    COMMISSION_ONLY("COMMISSION_ONLY", "纯提成"),
+    RENT_COMMISSION("RENT_COMMISSION", "租金+提成"),
+    RENT_COMMISSION_ELECTRIC("RENT_COMMISSION_ELECTRIC", "租金+提成+电费");
+
+    private final String code;
+    private final String description;
+
+    BillingMode(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public static BillingMode fromCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        for (BillingMode billingMode : values()) {
+            if (billingMode.code.equals(code)) {
+                return billingMode;
+            }
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        BillingMode billingMode = fromCode(code);
+        return billingMode != null ? billingMode.description : "未知";
+    }
+}

+ 41 - 0
haha-common/src/main/java/com/haha/common/enums/BusinessStatus.java

@@ -0,0 +1,41 @@
+package com.haha.common.enums;
+
+public enum BusinessStatus {
+
+    NORMAL("NORMAL", "正常营业"),
+    SUSPENDED("SUSPENDED", "暂停营业"),
+    WITHDRAWN("WITHDRAWN", "已撤场");
+
+    private final String code;
+    private final String description;
+
+    BusinessStatus(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public static BusinessStatus fromCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        for (BusinessStatus businessStatus : values()) {
+            if (businessStatus.code.equals(code)) {
+                return businessStatus;
+            }
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        BusinessStatus businessStatus = fromCode(code);
+        return businessStatus != null ? businessStatus.description : "未知";
+    }
+}

+ 41 - 0
haha-common/src/main/java/com/haha/common/enums/CommissionBaseType.java

@@ -0,0 +1,41 @@
+package com.haha.common.enums;
+
+public enum CommissionBaseType {
+
+    SALES("SALES", "门店实收销售额"),
+    GROSS_PROFIT("GROSS_PROFIT", "门店毛利总额"),
+    NET_REVENUE("NET_REVENUE", "扣除成本后营收");
+
+    private final String code;
+    private final String description;
+
+    CommissionBaseType(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public static CommissionBaseType fromCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        for (CommissionBaseType commissionBaseType : values()) {
+            if (commissionBaseType.code.equals(code)) {
+                return commissionBaseType;
+            }
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        CommissionBaseType commissionBaseType = fromCode(code);
+        return commissionBaseType != null ? commissionBaseType.description : "未知";
+    }
+}

+ 42 - 0
haha-common/src/main/java/com/haha/common/enums/ContractStatus.java

@@ -0,0 +1,42 @@
+package com.haha.common.enums;
+
+public enum ContractStatus {
+
+    PENDING("PENDING", "待签约"),
+    ACTIVE("ACTIVE", "已生效"),
+    EXPIRING("EXPIRING", "到期待续约"),
+    TERMINATED("TERMINATED", "已解约终止");
+
+    private final String code;
+    private final String description;
+
+    ContractStatus(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public static ContractStatus fromCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        for (ContractStatus contractStatus : values()) {
+            if (contractStatus.code.equals(code)) {
+                return contractStatus;
+            }
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        ContractStatus contractStatus = fromCode(code);
+        return contractStatus != null ? contractStatus.description : "未知";
+    }
+}

+ 41 - 0
haha-common/src/main/java/com/haha/common/enums/DepositStatus.java

@@ -0,0 +1,41 @@
+package com.haha.common.enums;
+
+public enum DepositStatus {
+
+    UNPAID("UNPAID", "未缴纳"),
+    PARTIAL("PARTIAL", "部分缴纳"),
+    FULL("FULL", "全额缴纳");
+
+    private final String code;
+    private final String description;
+
+    DepositStatus(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public static DepositStatus fromCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        for (DepositStatus depositStatus : values()) {
+            if (depositStatus.code.equals(code)) {
+                return depositStatus;
+            }
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        DepositStatus depositStatus = fromCode(code);
+        return depositStatus != null ? depositStatus.description : "未知";
+    }
+}

+ 40 - 0
haha-common/src/main/java/com/haha/common/enums/InvoiceType.java

@@ -0,0 +1,40 @@
+package com.haha.common.enums;
+
+public enum InvoiceType {
+
+    SPECIAL("SPECIAL", "增值税专票"),
+    NORMAL("NORMAL", "增值税普票");
+
+    private final String code;
+    private final String description;
+
+    InvoiceType(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public static InvoiceType fromCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        for (InvoiceType invoiceType : values()) {
+            if (invoiceType.code.equals(code)) {
+                return invoiceType;
+            }
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        InvoiceType invoiceType = fromCode(code);
+        return invoiceType != null ? invoiceType.description : "未知";
+    }
+}

+ 42 - 0
haha-common/src/main/java/com/haha/common/enums/PaymentCycle.java

@@ -0,0 +1,42 @@
+package com.haha.common.enums;
+
+public enum PaymentCycle {
+
+    MONTHLY("MONTHLY", "月付"),
+    QUARTERLY("QUARTERLY", "季度付"),
+    SEMI_ANNUAL("SEMI_ANNUAL", "半年付"),
+    ANNUAL("ANNUAL", "年付");
+
+    private final String code;
+    private final String description;
+
+    PaymentCycle(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public static PaymentCycle fromCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        for (PaymentCycle paymentCycle : values()) {
+            if (paymentCycle.code.equals(code)) {
+                return paymentCycle;
+            }
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        PaymentCycle paymentCycle = fromCode(code);
+        return paymentCycle != null ? paymentCycle.description : "未知";
+    }
+}

+ 40 - 0
haha-common/src/main/java/com/haha/common/enums/PaymentMethod.java

@@ -0,0 +1,40 @@
+package com.haha.common.enums;
+
+public enum PaymentMethod {
+
+    CORPORATE("CORPORATE", "对公"),
+    PRIVATE("PRIVATE", "对私");
+
+    private final String code;
+    private final String description;
+
+    PaymentMethod(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public static PaymentMethod fromCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        for (PaymentMethod paymentMethod : values()) {
+            if (paymentMethod.code.equals(code)) {
+                return paymentMethod;
+            }
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        PaymentMethod paymentMethod = fromCode(code);
+        return paymentMethod != null ? paymentMethod.description : "未知";
+    }
+}

+ 40 - 0
haha-common/src/main/java/com/haha/common/enums/VenueType.java

@@ -0,0 +1,40 @@
+package com.haha.common.enums;
+
+public enum VenueType {
+
+    CHARGING_STATION("CHARGING_STATION", "充电站"),
+    OFFICE_BUILDING("OFFICE_BUILDING", "写字楼");
+
+    private final String code;
+    private final String description;
+
+    VenueType(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public static VenueType fromCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        for (VenueType venueType : values()) {
+            if (venueType.code.equals(code)) {
+                return venueType;
+            }
+        }
+        return null;
+    }
+
+    public static String getDescription(String code) {
+        VenueType venueType = fromCode(code);
+        return venueType != null ? venueType.description : "未知";
+    }
+}

+ 318 - 0
haha-entity/src/main/java/com/haha/entity/ShopArchive.java

@@ -0,0 +1,318 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 门店档案实体类
+ */
+@Data
+@TableName("t_shop_archive")
+public class ShopArchive implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(type = IdType.ASSIGN_ID)
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /**
+     * 门店ID
+     */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long shopId;
+
+    /**
+     * 门店名称
+     */
+    private String shopName;
+
+    /**
+     * 门店地址
+     */
+    private String shopAddress;
+
+    /**
+     * 场地类型
+     */
+    private String venueType;
+
+    /**
+     * 合作模式
+     */
+    private String cooperationMode;
+
+    /**
+     * 乙方业务员
+     */
+    private String partyBSalesman;
+
+    /**
+     * 乙方业务员电话
+     */
+    private String partyBSalesmanPhone;
+
+    /**
+     * 甲方公司
+     */
+    private String partyACompany;
+
+    /**
+     * 甲方负责人
+     */
+    private String partyAManager;
+
+    /**
+     * 甲方负责人电话
+     */
+    private String partyAManagerPhone;
+
+    /**
+     * 营业状态
+     */
+    private String businessStatus;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 合同编号
+     */
+    private String contractNo;
+
+    /**
+     * 合同签订日期
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
+    private LocalDate contractSignDate;
+
+    /**
+     * 合同开始日期
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
+    private LocalDate contractStartDate;
+
+    /**
+     * 合同结束日期
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
+    private LocalDate contractEndDate;
+
+    /**
+     * 合同状态
+     */
+    private String contractStatus;
+
+    /**
+     * 起租日期
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
+    private LocalDate rentStartDate;
+
+    /**
+     * 月租金
+     */
+    private BigDecimal monthlyRent;
+
+    /**
+     * 免租天数
+     */
+    private Integer rentFreeDays;
+
+    /**
+     * 租金付款截止日
+     */
+    private Integer rentPayDeadlineDay;
+
+    /**
+     * 押金金额
+     */
+    private BigDecimal depositAmount;
+
+    /**
+     * 押金状态
+     */
+    private String depositStatus;
+
+    /**
+     * 押金已付金额
+     */
+    private BigDecimal depositPaidAmount;
+
+    /**
+     * 押金退还条件
+     */
+    private String depositRefundCondition;
+
+    /**
+     * 计费模式
+     */
+    private String billingMode;
+
+    /**
+     * 电价
+     */
+    private BigDecimal electricityPrice;
+
+    /**
+     * 电费分摊规则
+     */
+    private String electricityShareRule;
+
+    /**
+     * 提成基数类型
+     */
+    private String commissionBaseType;
+
+    /**
+     * 提成基数
+     */
+    private BigDecimal commissionBase;
+
+    /**
+     * 提成比例
+     */
+    private BigDecimal commissionRate;
+
+    /**
+     * 最低保底
+     */
+    private BigDecimal commissionMinGuarantee;
+
+    /**
+     * 提成周期(月)
+     */
+    private Integer commissionPeriodMonths;
+
+    /**
+     * 付款周期
+     */
+    private String paymentCycle;
+
+    /**
+     * 终止日期
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
+    private LocalDate terminationDate;
+
+    /**
+     * 终止备注
+     */
+    private String terminationRemark;
+
+    /**
+     * 付款方式
+     */
+    private String paymentMethod;
+
+    /**
+     * 收款户名
+     */
+    private String payeeAccountName;
+
+    /**
+     * 收款银行
+     */
+    private String payeeBankName;
+
+    /**
+     * 收款账号
+     */
+    private String payeeBankAccount;
+
+    /**
+     * 纳税人识别号
+     */
+    private String taxpayerId;
+
+    /**
+     * 发票类型
+     */
+    private String invoiceType;
+
+    /**
+     * 发票税率
+     */
+    private BigDecimal invoiceTaxRate;
+
+    /**
+     * 创建人ID
+     */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long creatorId;
+
+    /**
+     * 创建人姓名
+     */
+    private String creatorName;
+
+    /**
+     * 创建时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+
+    /**
+     * 编辑人ID
+     */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long editorId;
+
+    /**
+     * 编辑人姓名
+     */
+    private String editorName;
+
+    /**
+     * 更新时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime updateTime;
+
+    // ========== 以下为非数据库字段,用于前端展示 ==========
+
+    /**
+     * 固定资产列表
+     */
+    @TableField(exist = false)
+    private List<ShopFixedAsset> fixedAssets;
+
+    /**
+     * 合同附件列表
+     */
+    @TableField(exist = false)
+    private List<ShopContractAttachment> attachments;
+
+    /**
+     * 门店编码
+     */
+    @TableField(exist = false)
+    private String shopCode;
+
+    /**
+     * 合同状态标签
+     */
+    @TableField(exist = false)
+    private String contractStatusLabel;
+
+    /**
+     * 营业状态标签
+     */
+    @TableField(exist = false)
+    private String businessStatusLabel;
+}

+ 95 - 0
haha-entity/src/main/java/com/haha/entity/ShopContractAttachment.java

@@ -0,0 +1,95 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 门店合同附件实体类
+ */
+@Data
+@TableName("t_shop_contract_attachment")
+public class ShopContractAttachment implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(type = IdType.ASSIGN_ID)
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /**
+     * 门店档案ID
+     */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long shopArchiveId;
+
+    /**
+     * 门店ID
+     */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long shopId;
+
+    /**
+     * 附件类型
+     */
+    private String attachmentType;
+
+    /**
+     * 文件名
+     */
+    private String fileName;
+
+    /**
+     * 文件URL
+     */
+    private String fileUrl;
+
+    /**
+     * 文件大小
+     */
+    private Long fileSize;
+
+    /**
+     * 文件扩展名
+     */
+    private String fileExtension;
+
+    /**
+     * 排序
+     */
+    private Integer sortOrder;
+
+    /**
+     * 创建人ID
+     */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long creatorId;
+
+    /**
+     * 创建人姓名
+     */
+    private String creatorName;
+
+    /**
+     * 创建时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+
+    // ========== 以下为非数据库字段,用于前端展示 ==========
+
+    /**
+     * 附件类型标签
+     */
+    @TableField(exist = false)
+    private String attachmentTypeLabel;
+}

+ 113 - 0
haha-entity/src/main/java/com/haha/entity/ShopFixedAsset.java

@@ -0,0 +1,113 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 门店固定资产实体类
+ */
+@Data
+@TableName("t_shop_fixed_asset")
+public class ShopFixedAsset implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(type = IdType.ASSIGN_ID)
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /**
+     * 门店档案ID
+     */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long shopArchiveId;
+
+    /**
+     * 门店ID
+     */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long shopId;
+
+    /**
+     * 资产类型
+     */
+    private String assetType;
+
+    /**
+     * 资产名称
+     */
+    private String assetName;
+
+    /**
+     * 资产编码
+     */
+    private String assetCode;
+
+    /**
+     * 资产状态
+     */
+    private String assetStatus;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 创建人ID
+     */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long creatorId;
+
+    /**
+     * 创建人姓名
+     */
+    private String creatorName;
+
+    /**
+     * 创建时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+
+    /**
+     * 编辑人ID
+     */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long editorId;
+
+    /**
+     * 编辑人姓名
+     */
+    private String editorName;
+
+    /**
+     * 更新时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime updateTime;
+
+    // ========== 以下为非数据库字段,用于前端展示 ==========
+
+    /**
+     * 资产类型标签
+     */
+    @TableField(exist = false)
+    private String assetTypeLabel;
+
+    /**
+     * 资产状态标签
+     */
+    @TableField(exist = false)
+    private String assetStatusLabel;
+}

+ 25 - 0
haha-entity/src/main/java/com/haha/entity/dto/AttachmentDTO.java

@@ -0,0 +1,25 @@
+package com.haha.entity.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class AttachmentDTO {
+
+    private Long id;
+
+    @NotBlank(message = "附件类型不能为空")
+    private String attachmentType;
+
+    @NotBlank(message = "文件名不能为空")
+    private String fileName;
+
+    @NotBlank(message = "文件URL不能为空")
+    private String fileUrl;
+
+    private Long fileSize;
+
+    private String fileExtension;
+
+    private Integer sortOrder;
+}

+ 21 - 0
haha-entity/src/main/java/com/haha/entity/dto/FixedAssetDTO.java

@@ -0,0 +1,21 @@
+package com.haha.entity.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class FixedAssetDTO {
+
+    private Long id;
+
+    @NotBlank(message = "资产类型不能为空")
+    private String assetType;
+
+    private String assetName;
+
+    private String assetCode;
+
+    private String assetStatus;
+
+    private String remark;
+}

+ 108 - 0
haha-entity/src/main/java/com/haha/entity/dto/ShopArchiveCreateDTO.java

@@ -0,0 +1,108 @@
+package com.haha.entity.dto;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+
+@Data
+public class ShopArchiveCreateDTO {
+
+    @NotNull(message = "门店ID不能为空")
+    private Long shopId;
+
+    @NotBlank(message = "门店名称不能为空")
+    private String shopName;
+
+    private String shopAddress;
+
+    private String venueType;
+
+    private String cooperationMode;
+
+    private String partyBSalesman;
+
+    private String partyBSalesmanPhone;
+
+    private String partyACompany;
+
+    private String partyAManager;
+
+    private String partyAManagerPhone;
+
+    private String businessStatus;
+
+    private String remark;
+
+    private String contractNo;
+
+    private LocalDate contractSignDate;
+
+    private LocalDate contractStartDate;
+
+    private LocalDate contractEndDate;
+
+    private String contractStatus;
+
+    private LocalDate rentStartDate;
+
+    private BigDecimal monthlyRent;
+
+    private Integer rentFreeDays;
+
+    private Integer rentPayDeadlineDay;
+
+    private BigDecimal depositAmount;
+
+    private String depositStatus;
+
+    private BigDecimal depositPaidAmount;
+
+    private String depositRefundCondition;
+
+    private String billingMode;
+
+    private BigDecimal electricityPrice;
+
+    private String electricityShareRule;
+
+    private String commissionBaseType;
+
+    private BigDecimal commissionBase;
+
+    private BigDecimal commissionRate;
+
+    private BigDecimal commissionMinGuarantee;
+
+    private Integer commissionPeriodMonths;
+
+    private String paymentCycle;
+
+    private LocalDate terminationDate;
+
+    private String terminationRemark;
+
+    private String paymentMethod;
+
+    private String payeeAccountName;
+
+    private String payeeBankName;
+
+    private String payeeBankAccount;
+
+    private String taxpayerId;
+
+    private String invoiceType;
+
+    private BigDecimal invoiceTaxRate;
+
+    @Valid
+    private List<FixedAssetDTO> fixedAssets;
+
+    @Valid
+    private List<AttachmentDTO> attachments;
+}

+ 24 - 0
haha-entity/src/main/java/com/haha/entity/dto/ShopArchiveQueryDTO.java

@@ -0,0 +1,24 @@
+package com.haha.entity.dto;
+
+import com.haha.common.dto.BasePageQueryDTO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ShopArchiveQueryDTO extends BasePageQueryDTO {
+
+    private Long shopId;
+
+    private String shopName;
+
+    private String contractNo;
+
+    private String contractStatus;
+
+    private String businessStatus;
+
+    private String billingMode;
+
+    private String venueType;
+}

+ 111 - 0
haha-entity/src/main/java/com/haha/entity/dto/ShopArchiveUpdateDTO.java

@@ -0,0 +1,111 @@
+package com.haha.entity.dto;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+
+@Data
+public class ShopArchiveUpdateDTO {
+
+    @NotNull(message = "档案ID不能为空")
+    private Long id;
+
+    @NotNull(message = "门店ID不能为空")
+    private Long shopId;
+
+    @NotBlank(message = "门店名称不能为空")
+    private String shopName;
+
+    private String shopAddress;
+
+    private String venueType;
+
+    private String cooperationMode;
+
+    private String partyBSalesman;
+
+    private String partyBSalesmanPhone;
+
+    private String partyACompany;
+
+    private String partyAManager;
+
+    private String partyAManagerPhone;
+
+    private String businessStatus;
+
+    private String remark;
+
+    private String contractNo;
+
+    private LocalDate contractSignDate;
+
+    private LocalDate contractStartDate;
+
+    private LocalDate contractEndDate;
+
+    private String contractStatus;
+
+    private LocalDate rentStartDate;
+
+    private BigDecimal monthlyRent;
+
+    private Integer rentFreeDays;
+
+    private Integer rentPayDeadlineDay;
+
+    private BigDecimal depositAmount;
+
+    private String depositStatus;
+
+    private BigDecimal depositPaidAmount;
+
+    private String depositRefundCondition;
+
+    private String billingMode;
+
+    private BigDecimal electricityPrice;
+
+    private String electricityShareRule;
+
+    private String commissionBaseType;
+
+    private BigDecimal commissionBase;
+
+    private BigDecimal commissionRate;
+
+    private BigDecimal commissionMinGuarantee;
+
+    private Integer commissionPeriodMonths;
+
+    private String paymentCycle;
+
+    private LocalDate terminationDate;
+
+    private String terminationRemark;
+
+    private String paymentMethod;
+
+    private String payeeAccountName;
+
+    private String payeeBankName;
+
+    private String payeeBankAccount;
+
+    private String taxpayerId;
+
+    private String invoiceType;
+
+    private BigDecimal invoiceTaxRate;
+
+    @Valid
+    private List<FixedAssetDTO> fixedAssets;
+
+    @Valid
+    private List<AttachmentDTO> attachments;
+}

+ 21 - 0
haha-mapper/src/main/java/com/haha/mapper/ShopArchiveMapper.java

@@ -0,0 +1,21 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.ShopArchive;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 门店档案Mapper
+ */
+@Mapper
+public interface ShopArchiveMapper extends BaseMapper<ShopArchive> {
+
+    /**
+     * 根据门店ID查询档案
+     *
+     * @param shopId 门店ID
+     * @return 门店档案
+     */
+    ShopArchive selectByShopId(@Param("shopId") Long shopId);
+}

+ 12 - 0
haha-mapper/src/main/java/com/haha/mapper/ShopContractAttachmentMapper.java

@@ -0,0 +1,12 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.ShopContractAttachment;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 门店合同附件Mapper
+ */
+@Mapper
+public interface ShopContractAttachmentMapper extends BaseMapper<ShopContractAttachment> {
+}

+ 12 - 0
haha-mapper/src/main/java/com/haha/mapper/ShopFixedAssetMapper.java

@@ -0,0 +1,12 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.ShopFixedAsset;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 门店固定资产Mapper
+ */
+@Mapper
+public interface ShopFixedAssetMapper extends BaseMapper<ShopFixedAsset> {
+}

+ 66 - 0
haha-mapper/src/main/resources/mapper/ShopArchiveMapper.xml

@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.haha.mapper.ShopArchiveMapper">
+
+    <resultMap id="ShopArchiveResultMap" type="com.haha.entity.ShopArchive">
+        <id property="id" column="id"/>
+        <result property="shopId" column="shop_id"/>
+        <result property="shopName" column="shop_name"/>
+        <result property="shopAddress" column="shop_address"/>
+        <result property="venueType" column="venue_type"/>
+        <result property="cooperationMode" column="cooperation_mode"/>
+        <result property="partyBSalesman" column="party_b_salesman"/>
+        <result property="partyBSalesmanPhone" column="party_b_salesman_phone"/>
+        <result property="partyACompany" column="party_a_company"/>
+        <result property="partyAManager" column="party_a_manager"/>
+        <result property="partyAManagerPhone" column="party_a_manager_phone"/>
+        <result property="businessStatus" column="business_status"/>
+        <result property="remark" column="remark"/>
+        <result property="contractNo" column="contract_no"/>
+        <result property="contractSignDate" column="contract_sign_date"/>
+        <result property="contractStartDate" column="contract_start_date"/>
+        <result property="contractEndDate" column="contract_end_date"/>
+        <result property="contractStatus" column="contract_status"/>
+        <result property="rentStartDate" column="rent_start_date"/>
+        <result property="monthlyRent" column="monthly_rent"/>
+        <result property="rentFreeDays" column="rent_free_days"/>
+        <result property="rentPayDeadlineDay" column="rent_pay_deadline_day"/>
+        <result property="depositAmount" column="deposit_amount"/>
+        <result property="depositStatus" column="deposit_status"/>
+        <result property="depositPaidAmount" column="deposit_paid_amount"/>
+        <result property="depositRefundCondition" column="deposit_refund_condition"/>
+        <result property="billingMode" column="billing_mode"/>
+        <result property="electricityPrice" column="electricity_price"/>
+        <result property="electricityShareRule" column="electricity_share_rule"/>
+        <result property="commissionBaseType" column="commission_base_type"/>
+        <result property="commissionBase" column="commission_base"/>
+        <result property="commissionRate" column="commission_rate"/>
+        <result property="commissionMinGuarantee" column="commission_min_guarantee"/>
+        <result property="commissionPeriodMonths" column="commission_period_months"/>
+        <result property="paymentCycle" column="payment_cycle"/>
+        <result property="terminationDate" column="termination_date"/>
+        <result property="terminationRemark" column="termination_remark"/>
+        <result property="paymentMethod" column="payment_method"/>
+        <result property="payeeAccountName" column="payee_account_name"/>
+        <result property="payeeBankName" column="payee_bank_name"/>
+        <result property="payeeBankAccount" column="payee_bank_account"/>
+        <result property="taxpayerId" column="taxpayer_id"/>
+        <result property="invoiceType" column="invoice_type"/>
+        <result property="invoiceTaxRate" column="invoice_tax_rate"/>
+        <result property="creatorId" column="creator_id"/>
+        <result property="creatorName" column="creator_name"/>
+        <result property="createTime" column="create_time"/>
+        <result property="editorId" column="editor_id"/>
+        <result property="editorName" column="editor_name"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <!-- 根据门店ID查询档案 -->
+    <select id="selectByShopId" resultMap="ShopArchiveResultMap">
+        SELECT *
+        FROM t_shop_archive
+        WHERE shop_id = #{shopId}
+        LIMIT 1
+    </select>
+
+</mapper>

+ 18 - 0
haha-service/pom.xml

@@ -76,6 +76,24 @@
             <groupId>com.github.wechatpay-apiv3</groupId>
             <groupId>com.github.wechatpay-apiv3</groupId>
             <artifactId>wechatpay-java</artifactId>
             <artifactId>wechatpay-java</artifactId>
         </dependency>
         </dependency>
+
+        <!-- 阿里云 STS SDK -->
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>aliyun-java-sdk-sts</artifactId>
+            <version>3.1.2</version>
+        </dependency>
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>aliyun-java-sdk-core</artifactId>
+            <version>4.6.4</version>
+        </dependency>
+        <!-- 阿里云 OSS SDK -->
+        <dependency>
+            <groupId>com.aliyun.oss</groupId>
+            <artifactId>aliyun-sdk-oss</artifactId>
+            <version>3.18.1</version>
+        </dependency>
     </dependencies>
     </dependencies>
 
 
 </project>
 </project>

+ 25 - 0
haha-service/src/main/java/com/haha/service/OssService.java

@@ -0,0 +1,25 @@
+package com.haha.service;
+
+import java.util.Map;
+
+/**
+ * OSS 对象存储服务接口
+ */
+public interface OssService {
+
+    /**
+     * 获取前端直传 OSS 的临时凭证(STS Token)
+     *
+     * @param dir 上传目录前缀(如 "shop-archive/contract")
+     * @return 包含 accessKeyId、accessKeySecret、securityToken、
+     *         expiration、host、dir 的凭证信息
+     */
+    Map<String, String> getUploadCredential(String dir);
+
+    /**
+     * 根据 objectKey 删除 OSS 文件
+     *
+     * @param objectKey OSS 对象键(不含域名)
+     */
+    void deleteByObjectKey(String objectKey);
+}

+ 72 - 0
haha-service/src/main/java/com/haha/service/ShopArchiveService.java

@@ -0,0 +1,72 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.ShopArchive;
+import com.haha.entity.dto.ShopArchiveCreateDTO;
+import com.haha.entity.dto.ShopArchiveQueryDTO;
+import com.haha.entity.dto.ShopArchiveUpdateDTO;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 门店档案服务接口
+ */
+public interface ShopArchiveService extends IService<ShopArchive> {
+
+    /**
+     * 分页查询门店档案列表
+     *
+     * @param queryDTO 查询条件
+     * @return 分页结果
+     */
+    IPage<ShopArchive> getPage(ShopArchiveQueryDTO queryDTO);
+
+    /**
+     * 查询门店档案详情(含固定资产和附件)
+     *
+     * @param id 档案ID
+     * @return 档案详情
+     */
+    ShopArchive getDetailById(Long id);
+
+    /**
+     * 根据门店ID查询档案
+     *
+     * @param shopId 门店ID
+     * @return 门店档案
+     */
+    ShopArchive getByShopId(Long shopId);
+
+    /**
+     * 创建门店档案
+     *
+     * @param dto 创建参数
+     * @return 创建后的档案
+     */
+    ShopArchive create(ShopArchiveCreateDTO dto);
+
+    /**
+     * 更新门店档案
+     *
+     * @param dto 更新参数
+     * @return 更新后的档案
+     */
+    ShopArchive update(ShopArchiveUpdateDTO dto);
+
+    /**
+     * 删除门店档案
+     *
+     * @param id 档案ID
+     */
+    void delete(Long id);
+
+    /**
+     * 获取枚举选项列表
+     *
+     * @param enumType 枚举类型
+     * @return 枚举选项列表
+     */
+    List<Map<String, String>> getEnumOptions(String enumType);
+}

+ 123 - 0
haha-service/src/main/java/com/haha/service/impl/OssServiceImpl.java

@@ -0,0 +1,123 @@
+package com.haha.service.impl;
+
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import com.aliyuncs.DefaultAcsClient;
+import com.aliyuncs.auth.sts.AssumeRoleRequest;
+import com.aliyuncs.auth.sts.AssumeRoleResponse;
+import com.aliyuncs.http.MethodType;
+import com.aliyuncs.profile.DefaultProfile;
+import com.haha.common.config.OssConfig;
+import com.haha.service.OssService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * OSS 对象存储服务实现
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class OssServiceImpl implements OssService {
+
+    private final OssConfig ossConfig;
+
+    @Override
+    public Map<String, String> getUploadCredential(String dir) {
+        log.info("获取 OSS 上传凭证,dir: {}", dir);
+
+        try {
+            // 构造 STS 请求
+            DefaultProfile profile = DefaultProfile.getProfile(
+                    "",
+                    ossConfig.getAccessKeyId(),
+                    ossConfig.getAccessKeySecret()
+            );
+            DefaultAcsClient client = new DefaultAcsClient(profile);
+
+            AssumeRoleRequest request = new AssumeRoleRequest();
+            request.setSysMethod(MethodType.POST);
+            request.setRoleArn(ossConfig.getSts().getRoleArn());
+            request.setRoleSessionName(ossConfig.getSts().getRoleSessionName());
+            request.setDurationSeconds(Long.valueOf(ossConfig.getSts().getDurationSeconds()));
+
+            // 设置策略,限制只能上传到指定 bucket 和目录
+            String policy = "{\n" +
+                    "    \"Version\": \"1\",\n" +
+                    "    \"Statement\": [\n" +
+                    "        {\n" +
+                    "            \"Effect\": \"Allow\",\n" +
+                    "            \"Action\": [\n" +
+                    "                \"oss:PutObject\",\n" +
+                    "                \"oss:PostObject\"\n" +
+                    "            ],\n" +
+                    "            \"Resource\": [\n" +
+                    "                \"acs:oss:*:*:" + ossConfig.getBucketName() + "/" + dir + "/*\"\n" +
+                    "            ]\n" +
+                    "        }\n" +
+                    "    ]\n" +
+                    "}";
+            request.setPolicy(policy);
+
+            AssumeRoleResponse response = client.getAcsResponse(request);
+
+            // 构造返回结果
+            String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+            String uploadDir = dir + "/" + dateDir + "/";
+
+            Map<String, String> credential = new HashMap<>();
+            credential.put("accessKeyId", response.getCredentials().getAccessKeyId());
+            credential.put("accessKeySecret", response.getCredentials().getAccessKeySecret());
+            credential.put("securityToken", response.getCredentials().getSecurityToken());
+            credential.put("expiration", response.getCredentials().getExpiration());
+            credential.put("host", "https://" + ossConfig.getBucketName() + "." + ossConfig.getEndpoint());
+            credential.put("dir", uploadDir);
+
+            log.info("获取 OSS 上传凭证成功,uploadDir: {}", uploadDir);
+            return credential;
+
+        } catch (Exception e) {
+            log.error("获取 OSS 上传凭证失败", e);
+            throw new RuntimeException("获取上传凭证失败: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public void deleteByObjectKey(String objectKey) {
+        log.info("删除 OSS 文件,objectKey: {}", objectKey);
+
+        OSS ossClient = null;
+        try {
+            // 创建 OSS 客户端
+            String endpoint = ossConfig.getEndpoint();
+            ossClient = new OSSClientBuilder().build(
+                    endpoint,
+                    ossConfig.getAccessKeyId(),
+                    ossConfig.getAccessKeySecret()
+            );
+
+            // 删除文件
+            ossClient.deleteObject(ossConfig.getBucketName(), objectKey);
+
+            log.info("删除 OSS 文件成功,objectKey: {}", objectKey);
+
+        } catch (Exception e) {
+            log.error("删除 OSS 文件失败,objectKey: {}", objectKey, e);
+            throw new RuntimeException("删除文件失败: " + e.getMessage(), e);
+        } finally {
+            if (ossClient != null) {
+                try {
+                    ossClient.shutdown();
+                } catch (Exception e) {
+                    log.warn("关闭 OSS 客户端失败", e);
+                }
+            }
+        }
+    }
+}

+ 541 - 0
haha-service/src/main/java/com/haha/service/impl/ShopArchiveServiceImpl.java

@@ -0,0 +1,541 @@
+package com.haha.service.impl;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.common.enums.*;
+import com.haha.common.exception.BusinessException;
+import com.haha.entity.Shop;
+import com.haha.entity.ShopArchive;
+import com.haha.entity.ShopContractAttachment;
+import com.haha.entity.ShopFixedAsset;
+import com.haha.entity.dto.*;
+import com.haha.mapper.ShopArchiveMapper;
+import com.haha.mapper.ShopContractAttachmentMapper;
+import com.haha.mapper.ShopFixedAssetMapper;
+import com.haha.mapper.ShopMapper;
+import com.haha.service.ShopArchiveService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 门店档案服务实现类
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ShopArchiveServiceImpl extends ServiceImpl<ShopArchiveMapper, ShopArchive> implements ShopArchiveService {
+
+    private final ShopArchiveMapper shopArchiveMapper;
+    private final ShopFixedAssetMapper shopFixedAssetMapper;
+    private final ShopContractAttachmentMapper shopContractAttachmentMapper;
+    private final ShopMapper shopMapper;
+
+    @Override
+    public IPage<ShopArchive> getPage(ShopArchiveQueryDTO queryDTO) {
+        queryDTO.validate();
+        Page<ShopArchive> page = new Page<>(queryDTO.getPage(), queryDTO.getPageSize());
+
+        LambdaQueryWrapper<ShopArchive> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StringUtils.hasText(queryDTO.getShopName()), ShopArchive::getShopName, queryDTO.getShopName());
+        wrapper.eq(StringUtils.hasText(queryDTO.getContractNo()), ShopArchive::getContractNo, queryDTO.getContractNo());
+        wrapper.eq(StringUtils.hasText(queryDTO.getContractStatus()), ShopArchive::getContractStatus, queryDTO.getContractStatus());
+        wrapper.eq(StringUtils.hasText(queryDTO.getBusinessStatus()), ShopArchive::getBusinessStatus, queryDTO.getBusinessStatus());
+        wrapper.eq(StringUtils.hasText(queryDTO.getBillingMode()), ShopArchive::getBillingMode, queryDTO.getBillingMode());
+        wrapper.eq(StringUtils.hasText(queryDTO.getVenueType()), ShopArchive::getVenueType, queryDTO.getVenueType());
+        wrapper.eq(queryDTO.getShopId() != null, ShopArchive::getShopId, queryDTO.getShopId());
+        wrapper.orderByDesc(ShopArchive::getCreateTime);
+
+        IPage<ShopArchive> result = this.page(page, wrapper);
+
+        // 填充标签
+        for (ShopArchive archive : result.getRecords()) {
+            fillLabels(archive);
+        }
+
+        return result;
+    }
+
+    @Override
+    public ShopArchive getDetailById(Long id) {
+        ShopArchive archive = this.getById(id);
+        if (archive == null) {
+            throw new BusinessException(404, "门店档案不存在");
+        }
+
+        // 查询固定资产
+        List<ShopFixedAsset> fixedAssets = shopFixedAssetMapper.selectList(
+                new LambdaQueryWrapper<ShopFixedAsset>()
+                        .eq(ShopFixedAsset::getShopArchiveId, id)
+                        .orderByAsc(ShopFixedAsset::getCreateTime)
+        );
+        archive.setFixedAssets(fixedAssets);
+
+        // 查询附件
+        List<ShopContractAttachment> attachments = shopContractAttachmentMapper.selectList(
+                new LambdaQueryWrapper<ShopContractAttachment>()
+                        .eq(ShopContractAttachment::getShopArchiveId, id)
+                        .orderByAsc(ShopContractAttachment::getSortOrder)
+        );
+        archive.setAttachments(attachments);
+
+        // 填充标签
+        fillLabels(archive);
+
+        // 填充资产标签
+        if (fixedAssets != null) {
+            for (ShopFixedAsset asset : fixedAssets) {
+                asset.setAssetTypeLabel(AssetType.getDescription(asset.getAssetType()));
+                asset.setAssetStatusLabel(AssetStatus.getDescription(asset.getAssetStatus()));
+            }
+        }
+
+        // 填充附件标签
+        if (attachments != null) {
+            for (ShopContractAttachment attachment : attachments) {
+                attachment.setAttachmentTypeLabel(AttachmentType.getDescription(attachment.getAttachmentType()));
+            }
+        }
+
+        return archive;
+    }
+
+    @Override
+    public ShopArchive getByShopId(Long shopId) {
+        ShopArchive archive = shopArchiveMapper.selectByShopId(shopId);
+        if (archive != null) {
+            // 查询固定资产
+            List<ShopFixedAsset> fixedAssets = shopFixedAssetMapper.selectList(
+                    new LambdaQueryWrapper<ShopFixedAsset>()
+                            .eq(ShopFixedAsset::getShopArchiveId, archive.getId())
+                            .orderByAsc(ShopFixedAsset::getCreateTime)
+            );
+            archive.setFixedAssets(fixedAssets);
+
+            // 查询附件
+            List<ShopContractAttachment> attachments = shopContractAttachmentMapper.selectList(
+                    new LambdaQueryWrapper<ShopContractAttachment>()
+                            .eq(ShopContractAttachment::getShopArchiveId, archive.getId())
+                            .orderByAsc(ShopContractAttachment::getSortOrder)
+            );
+            archive.setAttachments(attachments);
+
+            fillLabels(archive);
+        }
+        return archive;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public ShopArchive create(ShopArchiveCreateDTO dto) {
+        // 检查门店是否存在
+        Shop shop = shopMapper.selectById(dto.getShopId());
+        if (shop == null) {
+            throw new BusinessException(404, "门店不存在");
+        }
+
+        // 检查档案是否已存在
+        ShopArchive existArchive = this.lambdaQuery()
+                .eq(ShopArchive::getShopId, dto.getShopId())
+                .one();
+        if (existArchive != null) {
+            throw new BusinessException(400, "该门店已存在档案");
+        }
+
+        // 构建档案实体
+        ShopArchive archive = new ShopArchive();
+        archive.setShopId(dto.getShopId());
+        archive.setShopName(dto.getShopName());
+        archive.setShopAddress(dto.getShopAddress());
+        archive.setVenueType(dto.getVenueType());
+        archive.setCooperationMode(dto.getCooperationMode());
+        archive.setPartyBSalesman(dto.getPartyBSalesman());
+        archive.setPartyBSalesmanPhone(dto.getPartyBSalesmanPhone());
+        archive.setPartyACompany(dto.getPartyACompany());
+        archive.setPartyAManager(dto.getPartyAManager());
+        archive.setPartyAManagerPhone(dto.getPartyAManagerPhone());
+        archive.setBusinessStatus(dto.getBusinessStatus());
+        archive.setRemark(dto.getRemark());
+        archive.setContractNo(dto.getContractNo());
+        archive.setContractSignDate(dto.getContractSignDate());
+        archive.setContractStartDate(dto.getContractStartDate());
+        archive.setContractEndDate(dto.getContractEndDate());
+        archive.setContractStatus(dto.getContractStatus());
+        archive.setRentStartDate(dto.getRentStartDate());
+        archive.setMonthlyRent(dto.getMonthlyRent());
+        archive.setRentFreeDays(dto.getRentFreeDays());
+        archive.setRentPayDeadlineDay(dto.getRentPayDeadlineDay());
+        archive.setDepositAmount(dto.getDepositAmount());
+        archive.setDepositStatus(dto.getDepositStatus());
+        archive.setDepositPaidAmount(dto.getDepositPaidAmount());
+        archive.setDepositRefundCondition(dto.getDepositRefundCondition());
+        archive.setBillingMode(dto.getBillingMode());
+        archive.setElectricityPrice(dto.getElectricityPrice());
+        archive.setElectricityShareRule(dto.getElectricityShareRule());
+        archive.setCommissionBaseType(dto.getCommissionBaseType());
+        archive.setCommissionBase(dto.getCommissionBase());
+        archive.setCommissionRate(dto.getCommissionRate());
+        archive.setCommissionMinGuarantee(dto.getCommissionMinGuarantee());
+        archive.setCommissionPeriodMonths(dto.getCommissionPeriodMonths());
+        archive.setPaymentCycle(dto.getPaymentCycle());
+        archive.setTerminationDate(dto.getTerminationDate());
+        archive.setTerminationRemark(dto.getTerminationRemark());
+        archive.setPaymentMethod(dto.getPaymentMethod());
+        archive.setPayeeAccountName(dto.getPayeeAccountName());
+        archive.setPayeeBankName(dto.getPayeeBankName());
+        archive.setPayeeBankAccount(dto.getPayeeBankAccount());
+        archive.setTaxpayerId(dto.getTaxpayerId());
+        archive.setInvoiceType(dto.getInvoiceType());
+        archive.setInvoiceTaxRate(dto.getInvoiceTaxRate());
+
+        // 设置门店编码
+        archive.setShopCode(shop.getShopCode());
+
+        // 设置创建人信息
+        Long currentUserId = StpUtil.getLoginIdAsLong();
+        archive.setCreatorId(currentUserId);
+        archive.setCreatorName(StpUtil.getLoginIdAsString());
+        archive.setCreateTime(LocalDateTime.now());
+        archive.setUpdateTime(LocalDateTime.now());
+
+        // 保存档案
+        this.save(archive);
+
+        // 保存固定资产
+        saveFixedAssets(archive.getId(), archive.getShopId(), dto.getFixedAssets(), currentUserId);
+
+        // 保存附件
+        saveAttachments(archive.getId(), archive.getShopId(), dto.getAttachments(), currentUserId);
+
+        log.info("创建门店档案成功: id={}, shopId={}, shopName={}", archive.getId(), archive.getShopId(), archive.getShopName());
+
+        return this.getDetailById(archive.getId());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public ShopArchive update(ShopArchiveUpdateDTO dto) {
+        // 检查档案是否存在
+        ShopArchive archive = this.getById(dto.getId());
+        if (archive == null) {
+            throw new BusinessException(404, "门店档案不存在");
+        }
+
+        // 检查门店是否存在
+        Shop shop = shopMapper.selectById(dto.getShopId());
+        if (shop == null) {
+            throw new BusinessException(404, "门店不存在");
+        }
+
+        // 更新档案字段
+        archive.setShopId(dto.getShopId());
+        archive.setShopName(dto.getShopName());
+        archive.setShopAddress(dto.getShopAddress());
+        archive.setVenueType(dto.getVenueType());
+        archive.setCooperationMode(dto.getCooperationMode());
+        archive.setPartyBSalesman(dto.getPartyBSalesman());
+        archive.setPartyBSalesmanPhone(dto.getPartyBSalesmanPhone());
+        archive.setPartyACompany(dto.getPartyACompany());
+        archive.setPartyAManager(dto.getPartyAManager());
+        archive.setPartyAManagerPhone(dto.getPartyAManagerPhone());
+        archive.setBusinessStatus(dto.getBusinessStatus());
+        archive.setRemark(dto.getRemark());
+        archive.setContractNo(dto.getContractNo());
+        archive.setContractSignDate(dto.getContractSignDate());
+        archive.setContractStartDate(dto.getContractStartDate());
+        archive.setContractEndDate(dto.getContractEndDate());
+        archive.setContractStatus(dto.getContractStatus());
+        archive.setRentStartDate(dto.getRentStartDate());
+        archive.setMonthlyRent(dto.getMonthlyRent());
+        archive.setRentFreeDays(dto.getRentFreeDays());
+        archive.setRentPayDeadlineDay(dto.getRentPayDeadlineDay());
+        archive.setDepositAmount(dto.getDepositAmount());
+        archive.setDepositStatus(dto.getDepositStatus());
+        archive.setDepositPaidAmount(dto.getDepositPaidAmount());
+        archive.setDepositRefundCondition(dto.getDepositRefundCondition());
+        archive.setBillingMode(dto.getBillingMode());
+        archive.setElectricityPrice(dto.getElectricityPrice());
+        archive.setElectricityShareRule(dto.getElectricityShareRule());
+        archive.setCommissionBaseType(dto.getCommissionBaseType());
+        archive.setCommissionBase(dto.getCommissionBase());
+        archive.setCommissionRate(dto.getCommissionRate());
+        archive.setCommissionMinGuarantee(dto.getCommissionMinGuarantee());
+        archive.setCommissionPeriodMonths(dto.getCommissionPeriodMonths());
+        archive.setPaymentCycle(dto.getPaymentCycle());
+        archive.setTerminationDate(dto.getTerminationDate());
+        archive.setTerminationRemark(dto.getTerminationRemark());
+        archive.setPaymentMethod(dto.getPaymentMethod());
+        archive.setPayeeAccountName(dto.getPayeeAccountName());
+        archive.setPayeeBankName(dto.getPayeeBankName());
+        archive.setPayeeBankAccount(dto.getPayeeBankAccount());
+        archive.setTaxpayerId(dto.getTaxpayerId());
+        archive.setInvoiceType(dto.getInvoiceType());
+        archive.setInvoiceTaxRate(dto.getInvoiceTaxRate());
+
+        // 更新门店编码
+        archive.setShopCode(shop.getShopCode());
+
+        // 设置编辑人信息
+        Long currentUserId = StpUtil.getLoginIdAsLong();
+        archive.setEditorId(currentUserId);
+        archive.setEditorName(StpUtil.getLoginIdAsString());
+        archive.setUpdateTime(LocalDateTime.now());
+
+        // 更新档案
+        this.updateById(archive);
+
+        // 更新固定资产:先删除再新增
+        shopFixedAssetMapper.delete(
+                new LambdaQueryWrapper<ShopFixedAsset>()
+                        .eq(ShopFixedAsset::getShopArchiveId, archive.getId())
+        );
+        saveFixedAssets(archive.getId(), archive.getShopId(), dto.getFixedAssets(), currentUserId);
+
+        // 更新附件:对比处理
+        updateAttachments(archive.getId(), archive.getShopId(), dto.getAttachments(), currentUserId);
+
+        log.info("更新门店档案成功: id={}, shopId={}, shopName={}", archive.getId(), archive.getShopId(), archive.getShopName());
+
+        return this.getDetailById(archive.getId());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void delete(Long id) {
+        ShopArchive archive = this.getById(id);
+        if (archive == null) {
+            throw new BusinessException(404, "门店档案不存在");
+        }
+
+        // 删除固定资产
+        shopFixedAssetMapper.delete(
+                new LambdaQueryWrapper<ShopFixedAsset>()
+                        .eq(ShopFixedAsset::getShopArchiveId, id)
+        );
+
+        // 删除附件
+        shopContractAttachmentMapper.delete(
+                new LambdaQueryWrapper<ShopContractAttachment>()
+                        .eq(ShopContractAttachment::getShopArchiveId, id)
+        );
+
+        // 删除档案
+        this.removeById(id);
+
+        log.info("删除门店档案成功: id={}, shopId={}", id, archive.getShopId());
+    }
+
+    @Override
+    public List<Map<String, String>> getEnumOptions(String enumType) {
+        List<Map<String, String>> options = new ArrayList<>();
+
+        switch (enumType) {
+            case "venueType":
+                for (VenueType e : VenueType.values()) {
+                    options.add(createOption(e.getCode(), e.getDescription()));
+                }
+                break;
+            case "businessStatus":
+                for (BusinessStatus e : BusinessStatus.values()) {
+                    options.add(createOption(e.getCode(), e.getDescription()));
+                }
+                break;
+            case "contractStatus":
+                for (ContractStatus e : ContractStatus.values()) {
+                    options.add(createOption(e.getCode(), e.getDescription()));
+                }
+                break;
+            case "depositStatus":
+                for (DepositStatus e : DepositStatus.values()) {
+                    options.add(createOption(e.getCode(), e.getDescription()));
+                }
+                break;
+            case "billingMode":
+                for (BillingMode e : BillingMode.values()) {
+                    options.add(createOption(e.getCode(), e.getDescription()));
+                }
+                break;
+            case "paymentCycle":
+                for (PaymentCycle e : PaymentCycle.values()) {
+                    options.add(createOption(e.getCode(), e.getDescription()));
+                }
+                break;
+            case "assetType":
+                for (AssetType e : AssetType.values()) {
+                    options.add(createOption(e.getCode(), e.getDescription()));
+                }
+                break;
+            case "assetStatus":
+                for (AssetStatus e : AssetStatus.values()) {
+                    options.add(createOption(e.getCode(), e.getDescription()));
+                }
+                break;
+            case "attachmentType":
+                for (AttachmentType e : AttachmentType.values()) {
+                    options.add(createOption(e.getCode(), e.getDescription()));
+                }
+                break;
+            case "paymentMethod":
+                for (PaymentMethod e : PaymentMethod.values()) {
+                    options.add(createOption(e.getCode(), e.getDescription()));
+                }
+                break;
+            case "invoiceType":
+                for (InvoiceType e : InvoiceType.values()) {
+                    options.add(createOption(e.getCode(), e.getDescription()));
+                }
+                break;
+            case "commissionBaseType":
+                for (CommissionBaseType e : CommissionBaseType.values()) {
+                    options.add(createOption(e.getCode(), e.getDescription()));
+                }
+                break;
+            default:
+                throw new BusinessException(400, "不支持的枚举类型: " + enumType);
+        }
+
+        return options;
+    }
+
+    /**
+     * 填充档案标签
+     */
+    private void fillLabels(ShopArchive archive) {
+        if (archive.getContractStatus() != null) {
+            archive.setContractStatusLabel(ContractStatus.getDescription(archive.getContractStatus()));
+        }
+        if (archive.getBusinessStatus() != null) {
+            archive.setBusinessStatusLabel(BusinessStatus.getDescription(archive.getBusinessStatus()));
+        }
+    }
+
+    /**
+     * 保存固定资产
+     */
+    private void saveFixedAssets(Long archiveId, Long shopId, List<FixedAssetDTO> dtos, Long currentUserId) {
+        if (dtos == null || dtos.isEmpty()) {
+            return;
+        }
+
+        for (FixedAssetDTO dto : dtos) {
+            ShopFixedAsset asset = new ShopFixedAsset();
+            asset.setShopArchiveId(archiveId);
+            asset.setShopId(shopId);
+            asset.setAssetType(dto.getAssetType());
+            asset.setAssetName(dto.getAssetName());
+            asset.setAssetCode(dto.getAssetCode());
+            asset.setAssetStatus(dto.getAssetStatus());
+            asset.setRemark(dto.getRemark());
+            asset.setCreatorId(currentUserId);
+            asset.setCreatorName(StpUtil.getLoginIdAsString());
+            asset.setCreateTime(LocalDateTime.now());
+            asset.setUpdateTime(LocalDateTime.now());
+            shopFixedAssetMapper.insert(asset);
+        }
+    }
+
+    /**
+     * 保存附件
+     */
+    private void saveAttachments(Long archiveId, Long shopId, List<AttachmentDTO> dtos, Long currentUserId) {
+        if (dtos == null || dtos.isEmpty()) {
+            return;
+        }
+
+        for (AttachmentDTO dto : dtos) {
+            ShopContractAttachment attachment = new ShopContractAttachment();
+            attachment.setShopArchiveId(archiveId);
+            attachment.setShopId(shopId);
+            attachment.setAttachmentType(dto.getAttachmentType());
+            attachment.setFileName(dto.getFileName());
+            attachment.setFileUrl(dto.getFileUrl());
+            attachment.setFileSize(dto.getFileSize());
+            attachment.setFileExtension(dto.getFileExtension());
+            attachment.setSortOrder(dto.getSortOrder());
+            attachment.setCreatorId(currentUserId);
+            attachment.setCreatorName(StpUtil.getLoginIdAsString());
+            attachment.setCreateTime(LocalDateTime.now());
+            shopContractAttachmentMapper.insert(attachment);
+        }
+    }
+
+    /**
+     * 更新附件(对比处理)
+     */
+    private void updateAttachments(Long archiveId, Long shopId, List<AttachmentDTO> dtos, Long currentUserId) {
+        // 查询现有附件
+        List<ShopContractAttachment> existingAttachments = shopContractAttachmentMapper.selectList(
+                new LambdaQueryWrapper<ShopContractAttachment>()
+                        .eq(ShopContractAttachment::getShopArchiveId, archiveId)
+        );
+
+        Set<Long> existingIds = existingAttachments.stream()
+                .map(ShopContractAttachment::getId)
+                .collect(Collectors.toSet());
+
+        Set<Long> dtoIds = dtos != null ? dtos.stream()
+                .map(AttachmentDTO::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet()) : Collections.emptySet();
+
+        // 删除不在DTO中的附件
+        for (Long existingId : existingIds) {
+            if (!dtoIds.contains(existingId)) {
+                shopContractAttachmentMapper.deleteById(existingId);
+            }
+        }
+
+        // 新增或更新附件
+        if (dtos != null) {
+            for (AttachmentDTO dto : dtos) {
+                if (dto.getId() == null) {
+                    // 新增
+                    ShopContractAttachment attachment = new ShopContractAttachment();
+                    attachment.setShopArchiveId(archiveId);
+                    attachment.setShopId(shopId);
+                    attachment.setAttachmentType(dto.getAttachmentType());
+                    attachment.setFileName(dto.getFileName());
+                    attachment.setFileUrl(dto.getFileUrl());
+                    attachment.setFileSize(dto.getFileSize());
+                    attachment.setFileExtension(dto.getFileExtension());
+                    attachment.setSortOrder(dto.getSortOrder());
+                    attachment.setCreatorId(currentUserId);
+                    attachment.setCreatorName(StpUtil.getLoginIdAsString());
+                    attachment.setCreateTime(LocalDateTime.now());
+                    shopContractAttachmentMapper.insert(attachment);
+                } else if (existingIds.contains(dto.getId())) {
+                    // 更新
+                    ShopContractAttachment attachment = shopContractAttachmentMapper.selectById(dto.getId());
+                    if (attachment != null) {
+                        attachment.setAttachmentType(dto.getAttachmentType());
+                        attachment.setFileName(dto.getFileName());
+                        attachment.setFileUrl(dto.getFileUrl());
+                        attachment.setFileSize(dto.getFileSize());
+                        attachment.setFileExtension(dto.getFileExtension());
+                        attachment.setSortOrder(dto.getSortOrder());
+                        shopContractAttachmentMapper.updateById(attachment);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 创建选项Map
+     */
+    private Map<String, String> createOption(String code, String description) {
+        Map<String, String> option = new HashMap<>();
+        option.put("code", code);
+        option.put("description", description);
+        return option;
+    }
+}

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.