add store

This commit is contained in:
Timi
2026-08-07 11:37:54 +08:00
parent 7ffdd715ee
commit 7e40e0202e
60 changed files with 1657 additions and 446 deletions
@@ -0,0 +1,192 @@
-- 新增门店主表。active_code / active_name 沿用项目现有软删除唯一索引方案:
-- 未删除数据参与唯一约束,已删除数据生成 NULL,不阻塞后续重建同编码或同名门店。
CREATE TABLE IF NOT EXISTS `gao_store` (
`id` VARCHAR(36) NOT NULL,
`code` VARCHAR(64) NOT NULL COMMENT '门店编码',
`active_code` VARCHAR(64) GENERATED ALWAYS AS (
CASE WHEN `deleted_at` IS NULL THEN NULLIF(TRIM(`code`), '') ELSE NULL END
) STORED COMMENT '未删除门店编码' INVISIBLE,
`name` VARCHAR(64) NOT NULL COMMENT '门店名称',
`active_name` VARCHAR(64) GENERATED ALWAYS AS (
CASE WHEN `deleted_at` IS NULL THEN NULLIF(TRIM(`name`), '') ELSE NULL END
) STORED COMMENT '未删除门店名称' INVISIBLE,
`status` VARCHAR(32) NOT NULL COMMENT '门店状态',
`telephone` VARCHAR(32) NULL COMMENT '电话',
`address` VARCHAR(255) NULL COMMENT '地址',
`remark` TEXT NULL COMMENT '备注',
`sort` INT NOT NULL DEFAULT 0 COMMENT '排序值',
`created_at` BIGINT NOT NULL,
`updated_at` BIGINT NULL,
`deleted_at` BIGINT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_gao_store_active_code` (`active_code`),
UNIQUE KEY `uk_gao_store_active_name` (`active_name`),
KEY `idx_gao_store_status_sort` (`status`, `deleted_at`, `sort`, `created_at`)
) COMMENT='GAO 门店';
-- 旧数据来自单门店模型,迁移时全部归入固定默认门店。
-- 使用固定 ID 而不是 UUID(),避免迁移中断后排查或手工重跑时产生多个默认门店。
SET @gao_default_store_id = '00000000-0000-0000-0000-000000000001';
SET @gao_now = ROUND(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000);
-- Flyway 正常不会重复执行版本脚本;这里保留 ON DUPLICATE KEY,方便本地或预发排查时手工重放片段。
INSERT INTO `gao_store` (
`id`,
`code`,
`name`,
`status`,
`telephone`,
`address`,
`remark`,
`sort`,
`created_at`
) VALUES (
@gao_default_store_id,
'DEFAULT',
'默认门店',
'ACTIVE',
NULL,
NULL,
'系统迁移自动创建',
0,
@gao_now
) ON DUPLICATE KEY UPDATE
`name` = VALUES(`name`),
`status` = VALUES(`status`),
`updated_at` = @gao_now;
-- 门店营业日历是连续登记统计的事实维度。
-- business_day_no 只给营业日使用,休息日 business_day_no 为 NULL。
-- 后续统计连续登记时按 business_day_no 连续,不按自然日连续。
CREATE TABLE IF NOT EXISTS `gao_store_business_day` (
`id` VARCHAR(36) NOT NULL,
`store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID',
`date_value` INT NOT NULL COMMENT '日期值,格式为 yyyyMMdd',
`active_date_value` INT GENERATED ALWAYS AS (
CASE WHEN `deleted_at` IS NULL THEN `date_value` ELSE NULL END
) STORED COMMENT '未删除日期值' INVISIBLE,
`open_status` VARCHAR(32) NOT NULL COMMENT '营业状态',
`business_day_no` BIGINT NULL COMMENT '营业日递增序号,仅营业日有值',
`active_business_day_no` BIGINT GENERATED ALWAYS AS (
CASE WHEN `deleted_at` IS NULL AND `open_status` = 'OPEN' THEN `business_day_no` ELSE NULL END
) STORED COMMENT '未删除营业日序号' INVISIBLE,
`reason` VARCHAR(255) NULL COMMENT '原因',
`created_at` BIGINT NOT NULL,
`updated_at` BIGINT NULL,
`deleted_at` BIGINT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_gao_store_business_day_active_date` (`store_id`, `active_date_value`),
UNIQUE KEY `uk_gao_store_business_day_active_no` (`store_id`, `active_business_day_no`),
KEY `idx_gao_store_business_day_open_no` (`store_id`, `open_status`, `business_day_no`)
) COMMENT='GAO 门店营业日';
-- 客户归属门店。先允许 NULL,加字段后回填,再改 NOT NULL,避免已有数据直接违反约束。
ALTER TABLE `gao_customer`
ADD COLUMN `store_id` VARCHAR(36) NULL COMMENT '门店 ID' AFTER `id`;
UPDATE `gao_customer`
SET `store_id` = @gao_default_store_id
WHERE `store_id` IS NULL;
ALTER TABLE `gao_customer`
MODIFY COLUMN `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID',
-- 客户编码、姓名从全局唯一改为门店内唯一。
DROP KEY `uk_gao_customer_active_code`,
DROP KEY `uk_gao_customer_active_name`,
ADD UNIQUE KEY `uk_gao_customer_store_active_code` (`store_id`, `active_code`),
ADD UNIQUE KEY `uk_gao_customer_store_active_name` (`store_id`, `active_name`),
ADD KEY `idx_gao_customer_store_created_at` (`store_id`, `deleted_at`, `created_at`);
-- 登记事件归属门店。历史事件统一归入默认门店。
ALTER TABLE `gao_event`
ADD COLUMN `store_id` VARCHAR(36) NULL COMMENT '门店 ID' AFTER `id`;
UPDATE `gao_event`
SET `store_id` = @gao_default_store_id
WHERE `store_id` IS NULL;
ALTER TABLE `gao_event`
MODIFY COLUMN `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID',
ADD KEY `idx_gao_event_store_status` (`store_id`, `status`, `deleted_at`, `sort`, `created_at`);
-- 登记记录是事实表,需要保存门店和营业日快照。
-- store_id:优先跟随历史登记事件所属门店,找不到事件时兜底默认门店。
-- registered_date_value:由 registered_at 计算 yyyyMMdd,避免查询时反复做时间函数。
-- business_day_no:按门店营业日历回填,用于连续登记统计。
ALTER TABLE `gao_event_record`
ADD COLUMN `store_id` VARCHAR(36) NULL COMMENT '门店 ID' AFTER `customer_id`,
ADD COLUMN `registered_date_value` INT NULL COMMENT '登记日期值,格式为 yyyyMMdd' AFTER `registered_at`,
ADD COLUMN `business_day_no` BIGINT NULL COMMENT '营业日递增序号' AFTER `registered_date_value`;
UPDATE `gao_event_record` r
LEFT JOIN `gao_event` e ON r.`event_id` = e.`id`
SET
r.`store_id` = COALESCE(e.`store_id`, @gao_default_store_id),
r.`registered_date_value` = CAST(DATE_FORMAT(FROM_UNIXTIME(r.`registered_at` / 1000), '%Y%m%d') AS UNSIGNED)
WHERE
r.`store_id` IS NULL
OR r.`registered_date_value` IS NULL;
-- 以历史登记记录中实际发生过登记的日期生成营业日。
-- 这是最平滑的迁移口径:有登记的日期一定视为营业日;没有登记的历史日期不主动生成休息日或营业日。
-- 如果门店历史休息日需要展示,可在迁移后通过营业日接口补录 CLOSED 数据,不影响已有登记记录。
INSERT INTO `gao_store_business_day` (
`id`,
`store_id`,
`date_value`,
`open_status`,
`business_day_no`,
`reason`,
`created_at`
)
SELECT
UUID(),
d.`store_id`,
d.`registered_date_value`,
'OPEN',
ROW_NUMBER() OVER (PARTITION BY d.`store_id` ORDER BY d.`registered_date_value`),
'系统迁移按历史登记日期生成',
@gao_now
FROM (
SELECT DISTINCT
`store_id`,
`registered_date_value`
FROM `gao_event_record`
WHERE
`store_id` IS NOT NULL
AND `registered_date_value` IS NOT NULL
) d;
-- 把营业日序号快照回填到历史登记记录。
-- 后续即使营业日历被调整,历史记录仍保留迁移时的事实快照;如需重算,应做显式重算脚本。
UPDATE `gao_event_record` r
LEFT JOIN `gao_store_business_day` d ON r.`store_id` = d.`store_id` AND r.`registered_date_value` = d.`date_value` AND d.`deleted_at` IS NULL
SET r.`business_day_no` = d.`business_day_no`
WHERE r.`business_day_no` IS NULL;
ALTER TABLE `gao_event_record`
MODIFY COLUMN `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID',
MODIFY COLUMN `registered_date_value` INT NOT NULL COMMENT '登记日期值,格式为 yyyyMMdd',
MODIFY COLUMN `business_day_no` BIGINT NOT NULL COMMENT '营业日递增序号',
ADD KEY `idx_gao_event_record_store_registered_at` (`store_id`, `deleted_at`, `registered_at`),
ADD KEY `idx_gao_event_record_store_date` (`store_id`, `registered_date_value`, `deleted_at`),
ADD KEY `idx_gao_event_record_store_customer_time` (`store_id`, `customer_id`, `deleted_at`, `registered_at`),
ADD KEY `idx_gao_event_record_store_event_time` (`store_id`, `event_id`, `deleted_at`, `registered_at`),
ADD KEY `idx_gao_event_record_store_customer_day` (`store_id`, `customer_id`, `business_day_no`);
-- GAO 用户归属门店。用户可在不同门店分别存在一条 GAO 用户记录。
-- 注意:当前用户角色关系仍是用户维度,不是门店维度;不同门店不同角色需要后续扩展 user_role 作用域。
ALTER TABLE `gao_user`
ADD COLUMN `store_id` VARCHAR(36) NULL COMMENT '门店 ID' AFTER `id`,
ADD COLUMN `active_user_id` VARCHAR(36) GENERATED ALWAYS AS (
CASE WHEN `deleted_at` IS NULL THEN `user_id` ELSE NULL END
) STORED COMMENT '未删除用户 ID' INVISIBLE;
UPDATE `gao_user`
SET `store_id` = @gao_default_store_id
WHERE `store_id` IS NULL;
ALTER TABLE `gao_user`
MODIFY COLUMN `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID',
ADD UNIQUE KEY `uk_gao_user_store_active_user` (`store_id`, `active_user_id`),
ADD KEY `idx_gao_user_store_enabled` (`store_id`, `enabled`, `deleted_at`, `created_at`);
@@ -0,0 +1,8 @@
-- V24 已经上线为“用户可在多个门店分别存在 GAO 用户记录”。
-- 现在业务改为一个用户只能归属一个门店,所以不能再改 V24,必须通过 V25 切换唯一约束。
-- 如果线上已经存在同一 user_id 绑定多个未删除门店的数据,下面新增唯一索引会失败。
-- 这是正确行为:这种数据无法自动判断保留哪个门店,必须人工先清理重复 GAO 用户记录。
ALTER TABLE `gao_user`
DROP KEY `uk_gao_user_store_active_user`,
ADD UNIQUE KEY `uk_gao_user_active_user` (`active_user_id`);
@@ -6,10 +6,16 @@
*
FROM `gao_customer`
WHERE
`id` IN
<foreach collection="idList" item="id" open="(" separator="," close=")">
#{id}
</foreach>
TRUE
<if test="idList != null and !idList.isEmpty()">
AND `id` IN
<foreach collection="idList" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</if>
<if test="idList == null or idList.isEmpty()">
AND FALSE
</if>
AND `deleted_at` IS NULL
</select>
@@ -19,7 +25,8 @@
COUNT(1) AS `newCustomerCount`
FROM `gao_customer`
WHERE
`created_at` >= #{beginCreatedAt}
`store_id` = #{storeId}
AND `created_at` >= #{beginCreatedAt}
AND `created_at` &lt;= #{endCreatedAt}
AND `deleted_at` IS NULL
GROUP BY DATE_FORMAT(FROM_UNIXTIME(`created_at` / 1000), '%Y%m%d')
@@ -31,7 +38,8 @@
COUNT(1)
FROM `gao_customer`
WHERE
`created_at` &lt; #{beginCreatedAt}
`store_id` = #{storeId}
AND `created_at` &lt; #{beginCreatedAt}
AND `deleted_at` IS NULL
</select>
@@ -41,7 +49,8 @@
COUNT(1) AS `count`
FROM `gao_customer`
WHERE
`deleted_at` IS NULL
`store_id` = #{storeId}
AND `deleted_at` IS NULL
GROUP BY IFNULL(CAST(`gender` AS CHAR), 'UNKNOWN')
</select>
</mapper>
@@ -24,7 +24,8 @@
*
FROM `gao_event`
WHERE
`status` = '${@com.imyeyu.api.modules.gao.entity.GaoEvent$Status@ACTIVE.name()}'
`store_id` = #{storeId}
AND `status` = '${@com.imyeyu.api.modules.gao.entity.GaoEvent$Status@ACTIVE.name()}'
AND (`begin_at` IS NULL OR `begin_at` &lt;= #{now})
AND (`end_at` IS NULL OR #{now} &lt;= `end_at`)
AND `deleted_at` IS NULL
@@ -8,6 +8,9 @@
`gao_event_record`
WHERE
TRUE
<if test="storeId != null">
AND `store_id` = #{storeId}
</if>
<if test="customerId != null">
AND `customer_id` = #{customerId}
</if>
@@ -18,19 +21,48 @@
AND `registered_at` &gt;= #{beginAt}
</if>
<if test="endAt != null">
AND `registered_at` &gt;= #{endAt}
AND `registered_at` &lt;= #{endAt}
</if>
AND `deleted_at` IS NULL
</select>
<select id="countByStoreIdAndRegisteredDateValue" resultType="java.lang.Long">
SELECT
COUNT(1)
FROM `gao_event_record`
WHERE
`store_id` = #{storeId}
AND `registered_date_value` = #{registeredDateValue}
AND `deleted_at` IS NULL
</select>
<update id="updateBusinessDayNoByStoreIdAndRegisteredDateValue">
UPDATE `gao_event_record`
SET
`business_day_no` = #{businessDayNo},
`updated_at` = ROUND(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000)
WHERE
`store_id` = #{storeId}
AND `registered_date_value` = #{registeredDateValue}
AND `deleted_at` IS NULL
</update>
<sql id="rangeFrom">
FROM `gao_event_record` r
LEFT JOIN `gao_customer` c ON r.`customer_id` = c.`id`
</sql>
<sql id="rangeWhere">
r.`registered_at` &gt;= #{beginAt}
AND r.`registered_at` &lt;= #{endAt}
TRUE
<if test="beginAt != null">
AND r.`registered_at` &gt;= #{beginAt}
</if>
<if test="endAt != null">
AND r.`registered_at` &lt;= #{endAt}
</if>
AND r.`deleted_at` IS NULL
<if test="storeId != null and storeId != ''">
AND r.`store_id` = #{storeId}
</if>
<if test="customerId != null">
AND r.`customer_id` = #{customerId}
</if>
@@ -69,6 +101,9 @@
<sql id="rankWhere">
r.`deleted_at` IS NULL
AND c.`deleted_at` IS NULL
<if test="storeId != null and storeId != ''">
AND r.`store_id` = #{storeId}
</if>
<if test="customerId != null and customerId != ''">
AND r.`customer_id` = #{customerId}
</if>
@@ -97,7 +132,7 @@
WITH `daily_stat` AS (
SELECT
r.`customer_id` AS `customerId`,
DATE(FROM_UNIXTIME(r.`registered_at` / 1000)) AS `eventDate`,
r.`business_day_no` AS `businessDayNo`,
COUNT(1) AS `dailyCount`,
MAX(r.`registered_at`) AS `latestRegisteredAt`
FROM `gao_event_record` r
@@ -106,7 +141,7 @@
<include refid="rankWhere"/>
GROUP BY
r.`customer_id`,
DATE(FROM_UNIXTIME(r.`registered_at` / 1000))
r.`business_day_no`
),
`rank_stat` AS (
SELECT
@@ -120,14 +155,14 @@
`continuous_ordered` AS (
SELECT
`customerId`,
`eventDate`,
ROW_NUMBER() OVER (PARTITION BY `customerId` ORDER BY `eventDate` DESC) AS `dayIndex`
`businessDayNo`,
ROW_NUMBER() OVER (PARTITION BY `customerId` ORDER BY `businessDayNo` DESC) AS `dayIndex`
FROM `daily_stat`
),
`latest_date` AS (
SELECT
`customerId`,
MAX(`eventDate`) AS `latestEventDate`
MAX(`businessDayNo`) AS `latestBusinessDayNo`
FROM `daily_stat`
GROUP BY `customerId`
),
@@ -137,7 +172,7 @@
COUNT(1) AS `recentContinuousEventDays`
FROM `continuous_ordered` o
LEFT JOIN `latest_date` l ON o.`customerId` = l.`customerId`
WHERE DATE_ADD(o.`eventDate`, INTERVAL o.`dayIndex` DAY) = DATE_ADD(l.`latestEventDate`, INTERVAL 1 DAY)
WHERE o.`businessDayNo` + o.`dayIndex` = l.`latestBusinessDayNo` + 1
GROUP BY o.`customerId`
),
`ranked` AS (
@@ -0,0 +1,17 @@
<?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.imyeyu.api.modules.gao.mapper.GaoStoreMapper">
<select id="selectByUserId" resultType="com.imyeyu.api.modules.gao.entity.GaoStore">
SELECT
s.*
FROM `gao_user` u
INNER JOIN `gao_store` s ON u.`store_id` = s.`id`
WHERE
u.`user_id` = #{userId}
AND u.`enabled` = TRUE
AND u.`deleted_at` IS NULL
AND s.`status` = 'ACTIVE'
AND s.`deleted_at` IS NULL
LIMIT 1
</select>
</mapper>
@@ -6,7 +6,7 @@
*
FROM `role`
WHERE
1 = 1
TRUE
<if test="idList != null and !idList.isEmpty()">
AND `id` IN
<foreach collection="idList" item="item" separator="," open="(" close=")">
@@ -14,29 +14,10 @@
</foreach>
</if>
<if test="idList == null or idList.isEmpty()">
AND 1 = 0
AND FALSE
</if>
AND `deleted_at` IS NULL
</select>
<select id="selectByModuleCodeAndCodeList" resultType="com.imyeyu.api.modules.user.entity.Role">
SELECT
*
FROM `role`
WHERE
1 = 1
AND `module_code` = #{moduleCode}
<if test="codeList != null and !codeList.isEmpty()">
AND `code` IN
<foreach collection="codeList" item="item" separator="," open="(" close=")">
#{item}
</foreach>
</if>
<if test="codeList == null or codeList.isEmpty()">
AND 1 = 0
</if>
AND `deleted_at` IS NULL
</select>
<select id="selectByModuleCode" resultType="com.imyeyu.api.modules.user.entity.Role">
SELECT
*
@@ -44,6 +25,6 @@
WHERE
`module_code` = #{moduleCode}
AND `deleted_at` IS NULL
ORDER BY `created_at` ASC
ORDER BY `created_at`
</select>
</mapper>
@@ -12,6 +12,23 @@
(#{item.id}, #{item.userId}, #{item.roleId}, #{item.createdAt})
</foreach>
</insert>
<select id="selectByUserIdList" resultType="com.imyeyu.api.modules.user.entity.UserRole">
SELECT
*
FROM `user_role`
WHERE
TRUE
<if test="userIdList != null and !userIdList.isEmpty()">
AND `user_id` IN
<foreach collection="userIdList" item="item" separator="," open="(" close=")">
#{item}
</foreach>
</if>
<if test="userIdList == null or userIdList.isEmpty()">
AND FALSE
</if>
AND `deleted_at` IS NULL
</select>
<update id="deleteByRoleIdList">
UPDATE `user_role`
SET `deleted_at` = UNIX_TIMESTAMP()