|
|
@@ -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 凭证签发与直传测试
|
|
|
+- [ ] 附件上传/预览/下载/删除测试
|
|
|
+- [ ] 权限校验测试
|
|
|
+- [ ] 字段联动逻辑测试
|