本文档为门店档案功能的完整需求设计,涵盖数据库设计、实体类、枚举、API 接口、OSS 文件上传方案。 门店档案用于管理门店的基础信息、固定资产、合同信息、结算开票信息及合同附件。
现有 t_shop 表仅包含门店基础字段(名称、地址、联系人等),无法满足业务上对门店合同、租金、结算、附件等档案信息的管理需求。本模块在现有门店体系基础上扩展,新增门店档案主表、固定资产子表、附件子表,实现门店全生命周期档案管理。
| 现有模块 | 关系说明 |
|---|---|
t_shop(门店表) |
门店档案通过 shop_id 关联,一个门店对应一条档案记录 |
t_device(设备表) |
固定资产中的"自助售卖机"与设备表独立管理,档案中的设备信息侧重商务属性(机器编号、状态),不替代设备表的技术属性 |
| OssUploadController | 新增 OSS 上传凭证接口,前端获取 STS Token 后直传 OSS,后端仅保存 URL |
门店档案
├── 一、门店基础信息(档案主表字段)
├── 二、固定资产设备信息(固定资产子表,1:N)
├── 三、合同核心信息(档案主表字段)
├── 四、甲方收款结算 & 开票信息(档案主表字段)
├── 五、合同附件归档(附件子表,1:N)
└── 六、系统溯源字段(自动回填)
t_shop_archiveCREATE 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='门店档案主表';
t_shop_fixed_assetCREATE 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='门店固定资产设备表';
t_shop_contract_attachmentCREATE 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='门店合同附件表';
VenueType位置: haha-common/src/main/java/com/haha/common/enums/VenueType.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 : "未知";
}
}
BusinessStatus位置: haha-common/src/main/java/com/haha/common/enums/BusinessStatus.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 : "未知";
}
}
ContractStatus位置: haha-common/src/main/java/com/haha/common/enums/ContractStatus.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 : "未知";
}
}
DepositStatus位置: haha-common/src/main/java/com/haha/common/enums/DepositStatus.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 : "未知";
}
}
BillingMode位置: haha-common/src/main/java/com/haha/common/enums/BillingMode.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 : "未知";
}
}
PaymentCycle位置: haha-common/src/main/java/com/haha/common/enums/PaymentCycle.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 : "未知";
}
}
AssetType位置: haha-common/src/main/java/com/haha/common/enums/AssetType.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 : "未知";
}
}
AssetStatus位置: haha-common/src/main/java/com/haha/common/enums/AssetStatus.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 : "未知";
}
}
AttachmentType位置: haha-common/src/main/java/com/haha/common/enums/AttachmentType.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 : "未知";
}
}
PaymentMethod位置: haha-common/src/main/java/com/haha/common/enums/PaymentMethod.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 : "未知";
}
}
InvoiceType位置: haha-common/src/main/java/com/haha/common/enums/InvoiceType.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 : "未知";
}
}
CommissionBaseType位置: haha-common/src/main/java/com/haha/common/enums/CommissionBaseType.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 : "未知";
}
}
位置: haha-entity/src/main/java/com/haha/entity/ShopArchive.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;
}
位置: haha-entity/src/main/java/com/haha/entity/ShopFixedAsset.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;
}
位置: haha-entity/src/main/java/com/haha/entity/ShopContractAttachment.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;
}
位置: haha-entity/src/main/java/com/haha/entity/dto/ShopArchiveCreateDTO.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;
}
位置: haha-entity/src/main/java/com/haha/entity/dto/ShopArchiveUpdateDTO.java
与 ShopArchiveCreateDTO 字段相同,额外增加:
@NotNull(message = "档案ID不能为空")
private Long id;
位置: haha-entity/src/main/java/com/haha/entity/dto/ShopArchiveQueryDTO.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;
}
位置: haha-entity/src/main/java/com/haha/entity/dto/FixedAssetDTO.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;
}
位置: haha-entity/src/main/java/com/haha/entity/dto/AttachmentDTO.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;
}
位置: haha-mapper/src/main/java/com/haha/mapper/ShopArchiveMapper.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);
}
位置: haha-mapper/src/main/java/com/haha/mapper/ShopFixedAssetMapper.java
package com.haha.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.haha.entity.ShopFixedAsset;
public interface ShopFixedAssetMapper extends BaseMapper<ShopFixedAsset> {
}
位置: haha-mapper/src/main/java/com/haha/mapper/ShopContractAttachmentMapper.java
package com.haha.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.haha.entity.ShopContractAttachment;
public interface ShopContractAttachmentMapper extends BaseMapper<ShopContractAttachment> {
}
位置: haha-mapper/src/main/resources/mapper/ShopArchiveMapper.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>
位置: haha-service/src/main/java/com/haha/service/ShopArchiveService.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);
}
位置: haha-service/src/main/java/com/haha/service/impl/ShopArchiveServiceImpl.java
核心业务逻辑要点:
创建档案:
shopId 对应的门店是否存在uk_shop_id 唯一约束)creatorId、creatorName(从 Sa-Token 获取当前登录管理员)更新档案:
editorId、editorNameshopArchiveId 删除旧记录,重新插入)删除档案:
deleted 字段,或在应用层标记状态)条件显示逻辑(前端根据以下规则控制字段显隐):
billingMode 包含电费相关(RENT_ELECTRIC / RENT_COMMISSION_ELECTRIC)时,显示电费单价和公摊规则billingMode 包含提成相关(COMMISSION_ONLY / RENT_COMMISSION / RENT_COMMISSION_ELECTRIC)时,显示提成基数、比例、保底金额、账期paymentMethod = CORPORATE 时,开户行必填位置: haha-admin/src/main/java/com/haha/admin/controller/ShopArchiveController.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));
}
}
| 操作 | 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 模式,文件不经过后端服务器中转:
──────────┐ ①请求上传凭证 ┌──────────┐
│ 前端 │ ──────────────────────→ │ 后端 │
│ │ ←────────────────────── │ │
│ │ ②返回STS Token │ │
│ │ │ │
│ │ ③直传文件到OSS │ │
│ │ ──────────────────────→ 阿里云 OSS │
│ │ │ │
│ │ ④上传成功,回调后端 │ │
│ │ ──────────────────────→ │ 后端 │
│ │ 保存 fileUrl │ 保存URL │
└──────────┘ └──────────┘
优势:
在 haha-service/pom.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>
在 application.yml 中添加:
# 阿里云 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分钟
位置: haha-common/src/main/java/com/haha/common/config/OssConfig.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;
}
}
接口位置: haha-service/src/main/java/com/haha/service/OssService.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/)ali-oss npm 包)直传fileUrl(domainPrefix + objectKey)提交给后端保存位置: haha-admin/src/main/java/com/haha/admin/controller/OssUploadController.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);
}
}
npm install ali-oss
位置: haha-admin-web/src/utils/ossUpload.ts
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('.', ''),
};
}
位置: haha-admin-web/src/api/shopArchive.ts
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 } });
}
<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>
前端附件区域按 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 |
支持多文件 |
| 权限标识 | 说明 |
|---|---|
shop:archive:read |
查看门店档案 |
shop:archive:create |
创建门店档案 |
shop:archive:update |
编辑门店档案 |
shop:archive:delete |
删除门店档案 |
在现有"运营管理 > 门店管理"下新增子菜单:
运营管理
├── 门店管理(现有)
├── 门店档案管理(新增)
│ ├── 查看
│ ├── 创建
│ ├── 编辑
│ └── 删除
├── 设备管理(现有)
├── ...
位置: docs/database/create_shop_archive.sql
包含三张表的完整建表语句(见第二章),以及初始菜单/权限数据插入。
采用分步 Tab 或折叠面板形式,按六大模块组织:
┌─────────────────────────────────────────────┐
│ Tab1: 门店基础信息 │
│ Tab2: 固定资产设备 │
│ Tab3: 合同核心信息 │
│ Tab4: 收款结算 & 开票 │
│ Tab5: 合同附件 │
└─────────────────────────────────────────────┘
字段联动规则:
在 haha-admin-web/src/router/modules/operation.ts 中新增:
{
path: "/operation/shop-archive",
name: "ShopArchiveList",
component: () => import("@/views/shop-archive/index.vue"),
meta: {
icon: "ri:file-text-line",
title: "门店档案管理"
}
}
t_shop_archive、t_shop_fixed_asset、t_shop_contract_attachment)ali-oss npm 依赖ossUpload.ts 上传工具类(凭证缓存 + 直传逻辑)shopArchive.ts