# 门店档案模块设计文档 > 本文档为门店档案功能的完整需求设计,涵盖数据库设计、实体类、枚举、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 fixedAssets; /** 关联的附件列表 */ @TableField(exist = false) private List 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 fixedAssets; // 附件列表 @Valid private List 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 { /** * 根据门店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 { } ``` ### 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 { } ``` ### 6.4 ShopArchiveMapper.xml **位置**: `haha-mapper/src/main/resources/mapper/ShopArchiveMapper.xml` ```xml ``` --- ## 七、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 { /** * 分页查询门店档案列表 */ IPage 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> list(ShopArchiveQueryDTO queryDTO) { queryDTO.validate(); return Result.success("查询成功", PageResult.of(shopArchiveService.getPage(queryDTO))); } /** * 查询门店档案详情 */ @RequirePermission("shop:archive:read") @GetMapping("/{id}") public Result 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 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 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 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 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> 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 com.aliyun aliyun-java-sdk-sts 3.1.2 com.aliyun aliyun-java-sdk-core 4.6.4 ``` ### 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 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> getUploadCredential( @RequestParam(value = "dir", defaultValue = "shop-archive") String dir) { Map credential = ossService.getUploadCredential(dir); return Result.success("获取凭证成功", credential); } /** * 删除 OSS 文件(根据 objectKey) */ @RequirePermission("shop:archive:delete") @DeleteMapping("/file") public Result 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 { 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 { 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 ``` ### 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 凭证签发与直传测试 - [ ] 附件上传/预览/下载/删除测试 - [ ] 权限校验测试 - [ ] 字段联动逻辑测试