Jelajahi Sumber

feat: t_user 表增加已关注公众号字段,admin-web 用户列表展示

- 新增 follow_official_account 字段(0=未关注, 1=已关注, 2=已取消关注)
- 关注/取消关注事件实时同步 t_user 关注状态
- 登录时通过 MpRelation 同步关注状态(覆盖先关注后注册等时序)
- 每日批量同步兜底事件丢失场景
- admin-web 用户列表新增"关注公众号"列,彩色标签展示

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 1 hari lalu
induk
melakukan
a8e57d383d

+ 4 - 0
admin-web/src/views/admin/account/index.vue

@@ -101,6 +101,9 @@
             <template v-if="['rechargeAmount','amount','refundAmount','balance','rechargeBalance','grantsBalance','frozenAmount','amountReceivable','amountReceived','discountAmount','refundDiscountAmount'].includes(field.prop)">
               {{ u.fmt.fmtMoney(row[field.prop]) }}
             </template>
+            <template v-else-if="'followOfficialAccount'===field.prop">
+             <ext-d-label type="User.followOfficialAccount" v-model="row[field.prop]"/>
+            </template>
             <template v-else-if="'status'===field.prop">
              <ext-d-label type="User.status" v-model="row[field.prop]"/>
             </template>
@@ -177,6 +180,7 @@ const state = reactive({
       {label: '赠金余额', width: 90, prop: 'grantsBalance', resizable: true, fixed: 'left'},
       {label: '冻结余额', width: 90, prop: 'frozenAmount', resizable: true, fixed: 'left'},
       {label: '状态', width: 80, prop: 'status', align: 'center'},
+      {label: '关注公众号', width: 110, prop: 'followOfficialAccount', align: 'center'},
       {label: '注册时间', width: 160, prop: 'registerTime', resizable: true},
       {label: '充值次数', width: 90, prop: 'rechargeTimes', resizable: true},
       {label: '充值金额', width: 90, prop: 'rechargeAmount', resizable: true},

+ 5 - 0
car-wash-entity/src/main/java/com/kym/entity/User.java

@@ -78,6 +78,11 @@ public class User extends BaseEntity implements Serializable {
      */
     private Integer status;
 
+    /**
+     * 已关注公众号:0-未关注 1-已关注 2-已取消关注
+     */
+    private Integer followOfficialAccount;
+
     /**
      * 最后登录时间
      */

+ 1 - 0
car-wash-entity/src/main/java/com/kym/entity/vo/CustomUserVo.java

@@ -57,5 +57,6 @@ public class CustomUserVo {
      * 退款扣除优惠总金额
      */
     private int refundDiscountAmount;
+    private Integer followOfficialAccount;
 
 }

+ 13 - 0
car-wash-entity/src/main/resources/sql/v14_follow_official_account.sql

@@ -0,0 +1,13 @@
+-- ====================================================
+-- t_user 表新增"已关注公众号"字段
+-- 0-未关注 1-已关注 2-已取消关注
+-- 发布日期:2026-06-15
+-- ====================================================
+
+ALTER TABLE `t_user`
+    ADD COLUMN `follow_official_account` INT DEFAULT 0 COMMENT '已关注公众号:0-未关注 1-已关注 2-已取消关注';
+
+INSERT INTO `t_data_dict` (`code`, `name`, `value`, `weight`, `remark`, `color`) VALUES
+('User.followOfficialAccount', '未关注', '0', 1, '公众号关注状态', 'info'),
+('User.followOfficialAccount', '已关注', '1', 2, '公众号关注状态', 'success'),
+('User.followOfficialAccount', '已取消关注', '2', 3, '公众号关注状态', 'warning');

+ 7 - 3
car-wash-mapper/src/main/resources/mappers/UserMapper.xml

@@ -18,6 +18,7 @@
         <result column="create_time" property="createTime"/>
         <result column="update_time" property="updateTime"/>
         <result column="last_login_time" property="lastLoginTime"/>
+        <result column="follow_official_account" property="followOfficialAccount"/>
     </resultMap>
 
     <resultMap id="CustomUserMap" type="com.kym.entity.vo.CustomUserVo">
@@ -35,11 +36,12 @@
         <result column="amount_receivable" property="amountReceivable"/>
         <result column="amount_received" property="amountReceived"/>
         <result column="discount_amount" property="discountAmount"/>
+        <result column="follow_official_account" property="followOfficialAccount"/>
     </resultMap>
 
     <!-- 通用查询结果列 -->
     <sql id="Base_Column_List">
-        id, company_id,openid, unionid, username, password, gender, nickname, mobile_phone, avatar, status, create_time, update_time, last_login_time
+        id, company_id,openid, unionid, username, password, gender, nickname, mobile_phone, avatar, status, create_time, update_time, last_login_time, follow_official_account
     </sql>
 
 
@@ -59,7 +61,8 @@
             t2.amount,
             t2.amount_receivable,
             t2.amount_received,
-            t2.discount_money
+            t2.discount_money,
+            t1.`follow_official_account`
             FROM
             t_user t1
             LEFT JOIN
@@ -115,7 +118,8 @@
             t1.amount,
             t1.amount_receivable,
             t1.amount_received,
-            t1.discount_money
+            t1.discount_money,
+            user.`follow_official_account`
             FROM t_user `user`
             LEFT JOIN
             (SELECT user_id,

+ 46 - 2
car-wash-service/src/main/java/com/kym/service/impl/MpRelationServiceImpl.java

@@ -54,7 +54,7 @@ public class MpRelationServiceImpl extends MyBaseServiceImpl<MpRelationMapper, M
             WxMpUserList wxUserList = wxMpService.getUserService().userList(nextOpenid);
             var openids = wxUserList.getOpenids();
             if (CommUtil.isEmptyOrNull(openids)) {
-                return;
+                break;
             }
 
             // 通过unionid获取公众号openid
@@ -83,10 +83,28 @@ public class MpRelationServiceImpl extends MyBaseServiceImpl<MpRelationMapper, M
 
             // 检查是否有下一页
             if (CommUtil.isEmptyOrNull(wxUserList.getNextOpenid())) {
-                return;
+                break;
             }
             nextOpenid = wxUserList.getNextOpenid();
         }
+        // 同步关注状态到 t_user(兜底事件丢失场景)
+        var allRelations = lambdaQuery().isNotNull(MpRelation::getUserId).list();
+        if (CommUtil.isNotEmptyOrNull(allRelations)) {
+            var subscribedIds = allRelations.stream()
+                    .filter(MpRelation::getSubscribe)
+                    .map(MpRelation::getUserId).toList();
+            if (!subscribedIds.isEmpty()) {
+                userService.lambdaUpdate().set(User::getFollowOfficialAccount, 1)
+                        .in(User::getId, subscribedIds).update();
+            }
+            var unsubscribedIds = allRelations.stream()
+                    .filter(r -> !r.getSubscribe())
+                    .map(MpRelation::getUserId).toList();
+            if (!unsubscribedIds.isEmpty()) {
+                userService.lambdaUpdate().set(User::getFollowOfficialAccount, 2)
+                        .in(User::getId, unsubscribedIds).update();
+            }
+        }
     }
 
     /**
@@ -116,9 +134,35 @@ public class MpRelationServiceImpl extends MyBaseServiceImpl<MpRelationMapper, M
                 mpRelation.setUserId(user.getId());
             }
             saveOrUpdate(mpRelation);
+            // 同步关注状态到 t_user
+            if (user != null) {
+                userService.lambdaUpdate()
+                        .set(User::getFollowOfficialAccount, 1)
+                        .eq(User::getId, user.getId())
+                        .update();
+            }
         } catch (WxErrorException e) {
             throw new RuntimeException(e);
         }
     }
 
+    /**
+     * 取消关注公众号
+     *
+     * @param mpOpenid 公众号openid
+     */
+    public void unbindMpUser(String mpOpenid) {
+        var mpRelation = lambdaQuery().eq(MpRelation::getMpOpenid, mpOpenid).one();
+        if (mpRelation != null) {
+            lambdaUpdate().set(MpRelation::getSubscribe, false)
+                    .eq(MpRelation::getMpOpenid, mpOpenid).update();
+            if (mpRelation.getUserId() != null) {
+                userService.lambdaUpdate()
+                        .set(User::getFollowOfficialAccount, 2)
+                        .eq(User::getId, mpRelation.getUserId())
+                        .update();
+            }
+        }
+    }
+
 }

+ 11 - 0
car-wash-service/src/main/java/com/kym/service/impl/UserServiceImpl.java

@@ -209,9 +209,20 @@ public class UserServiceImpl extends MPJBaseServiceImpl<UserMapper, User> implem
     protected void updateUnionid() {
         var unionid = StpUtil.getSession().getString("unionid");
         if (CommUtil.isNotEmptyAndNull(unionid)) {
+            // 查询现有关注状态
+            var mpRelation = mpRelationMapper.selectOne(
+                    new QueryWrapper<MpRelation>().eq("unionid", unionid));
             // 匹配公众号用户
             var wrapper = new LambdaUpdateWrapper<MpRelation>().eq(MpRelation::getUnionid, unionid).set(MpRelation::getUserId, StpUtil.getLoginIdAsLong()).set(MpRelation::getOpenid, StpUtil.getSession().getString("openid"));
             mpRelationMapper.update(null, wrapper);
+            // 同步关注状态到 t_user
+            if (mpRelation != null && mpRelation.getSubscribe() != null) {
+                Integer followStatus = mpRelation.getSubscribe() ? 1 : 2;
+                lambdaUpdate()
+                        .set(User::getFollowOfficialAccount, followStatus)
+                        .eq(User::getId, StpUtil.getLoginIdAsLong())
+                        .update();
+            }
         }
     }
 

+ 1 - 3
car-wash-service/src/main/java/com/kym/service/wechat/impl/WeixinMPServiceImpl.java

@@ -6,7 +6,6 @@ import cn.hutool.core.util.StrUtil;
 import cn.hutool.core.util.XmlUtil;
 import com.kym.common.utils.CommUtil;
 import com.kym.entity.MpMsgTemplate;
-import com.kym.entity.MpRelation;
 import com.kym.service.FaultSubscriberService;
 import com.kym.service.MpMsgTemplateService;
 import com.kym.service.impl.MpRelationServiceImpl;
@@ -90,8 +89,7 @@ public class WeixinMPServiceImpl implements WeixinMPService {
                         case "unsubscribe":
                             // 取消关注公众号
                             log.info("收到微信公众号取消关注通知: {}", mpOpenid);
-                            // 更新用户的关注状态
-                            mpRelationService.lambdaUpdate().set(MpRelation::getSubscribe, false).eq(MpRelation::getMpOpenid, mpOpenid).update();
+                            mpRelationService.unbindMpUser(mpOpenid);
                             break;
                         case "SCAN":
                             // 扫描二维码(微信推送上来的事件是大写 SCAN)