zjk
2026-09-29 54dcf0a82e7490fa58b1c056d5c0b9cb739c2931
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
package com.tcm.training.admin.app.member.service;
 
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.tcm.training.admin.app.auth.service.AppAuthSessionService;
import com.tcm.training.admin.app.member.dto.AppMemberPurchaseRequest;
import com.tcm.training.admin.app.member.vo.AppMemberPurchaseResultVO;
import com.tcm.training.admin.member.entity.AppUser;
import com.tcm.training.admin.member.entity.MemberPurchaseOrder;
import com.tcm.training.admin.member.mapper.MemberPurchaseOrderMapper;
import com.tcm.training.admin.member.mapper.UserMapper;
import com.tcm.training.admin.member.service.MemberSettingsAppService;
import com.tcm.training.admin.member.vo.MemberPriceSettingsVO;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
 
/**
 * APP 会员购买服务。
 */
@Service
public class AppMemberPurchaseService {
 
    private static final int USER_STATUS_ENABLED = 1;
    private static final String STATUS_PAID = "PAID";
    private static final String MEMBER_TYPE_MONTH_CARD = "MONTH_CARD";
    private static final String MEMBER_TYPE_QUARTER_CARD = "QUARTER_CARD";
    private static final String PAYMENT_METHOD_WECHAT = "WECHAT";
    private static final String PAYMENT_METHOD_ALIPAY = "ALIPAY";
    private static final DateTimeFormatter ORDER_NO_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
 
    private final AppAuthSessionService appAuthSessionService;
    private final UserMapper userMapper;
    private final MemberSettingsAppService memberSettingsAppService;
    private final MemberPurchaseOrderMapper memberPurchaseOrderMapper;
 
    /**
     * 注入 APP 会员购买所需依赖。
     */
    public AppMemberPurchaseService(AppAuthSessionService appAuthSessionService,
                                    UserMapper userMapper,
                                    MemberSettingsAppService memberSettingsAppService,
                                    MemberPurchaseOrderMapper memberPurchaseOrderMapper) {
        this.appAuthSessionService = appAuthSessionService;
        this.userMapper = userMapper;
        this.memberSettingsAppService = memberSettingsAppService;
        this.memberPurchaseOrderMapper = memberPurchaseOrderMapper;
    }
 
    /**
     * 提交 APP 会员购买,当前阶段按模拟直付处理并立即生效。
     */
    @Transactional
    public AppMemberPurchaseResultVO purchase(AppMemberPurchaseRequest request) {
        Long userId = appAuthSessionService.requireLoginUserId();
        AppUser user = requireEnabledUser(userId);
        MemberPriceSettingsVO settings = requirePriceSettings();
        String memberType = normalizeMemberType(request == null ? null : request.getMemberType());
        String paymentMethod = normalizePaymentMethod(request == null ? null : request.getPaymentMethod());
 
        PackageSnapshot packageSnapshot = resolveEnabledPackage(settings, memberType);
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime memberExpireAt = resolveMemberExpireAt(user.getMemberExpireAt(), packageSnapshot.getMemberDays(), now);
 
        MemberPurchaseOrder order = buildOrder(user, packageSnapshot, paymentMethod, now);
        memberPurchaseOrderMapper.insert(order);
 
        // 会员购买成功后立即更新用户到期时间,APP 可直接回写最新会员态。
        user.setMemberExpireAt(memberExpireAt);
        userMapper.updateById(user);
        order.setId(resolveOrderId(order, user.getId(), now));
 
        return buildPurchaseResult(order, memberExpireAt);
    }
 
    /**
     * 校验当前登录用户存在且可用。
     */
    private AppUser requireEnabledUser(Long userId) {
        AppUser user = userMapper.selectById(userId);
        if (user == null || user.getDeleted() == null || user.getDeleted().intValue() != 0) {
            throw new IllegalArgumentException("用户不存在");
        }
        if (user.getStatus() == null || user.getStatus().intValue() != USER_STATUS_ENABLED) {
            throw new IllegalArgumentException("用户已禁用");
        }
        return user;
    }
 
    /**
     * 读取当前会员价格配置,不存在时阻止下单。
     */
    private MemberPriceSettingsVO requirePriceSettings() {
        MemberPriceSettingsVO settings = memberSettingsAppService.currentPrice();
        if (settings == null) {
            throw new IllegalArgumentException("会员套餐暂未配置");
        }
        return settings;
    }
 
    /**
     * 校验并提取当前可购买的会员套餐快照。
     */
    private PackageSnapshot resolveEnabledPackage(MemberPriceSettingsVO settings, String memberType) {
        if (MEMBER_TYPE_MONTH_CARD.equals(memberType)) {
            return buildPackageSnapshot(memberType,
                    settings.getMonthCardEnabled(),
                    settings.getMonthCardDays(),
                    settings.getMonthCardPrice(),
                    "月会员暂未开放购买");
        }
        if (MEMBER_TYPE_QUARTER_CARD.equals(memberType)) {
            return buildPackageSnapshot(memberType,
                    settings.getQuarterCardEnabled(),
                    settings.getQuarterCardDays(),
                    settings.getQuarterCardPrice(),
                    "季度会员暂未开放购买");
        }
        throw new IllegalArgumentException("会员套餐不合法");
    }
 
    /**
     * 将后台启用配置转换为下单时使用的套餐快照。
     */
    private PackageSnapshot buildPackageSnapshot(String memberType,
                                                Boolean enabled,
                                                Integer memberDays,
                                                BigDecimal orderAmount,
                                                String disabledMessage) {
        if (!Boolean.TRUE.equals(enabled)) {
            throw new IllegalArgumentException(disabledMessage);
        }
        if (memberDays == null || memberDays.intValue() <= 0 || orderAmount == null || orderAmount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("会员套餐配置不完整");
        }
        return new PackageSnapshot(memberType, memberDays, orderAmount);
    }
 
    /**
     * 规范化会员套餐类型,仅允许月卡或季卡。
     */
    private String normalizeMemberType(String memberType) {
        String normalized = trimToEmpty(memberType).toUpperCase();
        if (MEMBER_TYPE_MONTH_CARD.equals(normalized) || MEMBER_TYPE_QUARTER_CARD.equals(normalized)) {
            return normalized;
        }
        throw new IllegalArgumentException("会员套餐不合法");
    }
 
    /**
     * 规范化支付方式入参,仅允许微信或支付宝。
     */
    private String normalizePaymentMethod(String paymentMethod) {
        String normalized = trimToEmpty(paymentMethod).toUpperCase();
        if (PAYMENT_METHOD_WECHAT.equals(normalized) || PAYMENT_METHOD_ALIPAY.equals(normalized)) {
            return normalized;
        }
        throw new IllegalArgumentException("支付方式不合法");
    }
 
    /**
     * 计算新的会员到期时间;有效会员按原到期时间顺延,过期会员从当前时间起算。
     */
    private LocalDateTime resolveMemberExpireAt(LocalDateTime currentExpireAt, Integer memberDays, LocalDateTime now) {
        LocalDateTime baseTime = currentExpireAt != null && currentExpireAt.isAfter(now) ? currentExpireAt : now;
        return baseTime.plusDays(memberDays == null ? 0L : memberDays.longValue());
    }
 
    /**
     * 组装会员购买订单,支付方案落地前直接记为已支付。
     */
    private MemberPurchaseOrder buildOrder(AppUser user,
                                           PackageSnapshot packageSnapshot,
                                           String paymentMethod,
                                           LocalDateTime now) {
        MemberPurchaseOrder order = new MemberPurchaseOrder();
        order.setOrderNo(buildOrderNo(user.getId(), now));
        order.setAppUserId(user.getId());
        order.setUserNickname(resolveUserNickname(user));
        order.setUserPhone(trimToEmpty(user.getPhone()));
        order.setMemberType(packageSnapshot.getMemberType());
        order.setMemberDays(packageSnapshot.getMemberDays());
        order.setOrderAmount(packageSnapshot.getOrderAmount());
        order.setPaymentMethod(paymentMethod);
        order.setRefundAmount(null);
        order.setRefundRemark(null);
        order.setStatus(STATUS_PAID);
        order.setPaidAt(now);
        order.setRefundedAt(null);
        order.setCancelledAt(null);
        order.setDeleted(0);
        return order;
    }
 
    /**
     * 生成会员订单号。
     */
    private String buildOrderNo(Long userId, LocalDateTime now) {
        String seed = String.valueOf(Math.abs((userId == null ? 0L : userId.longValue()) % 10000L));
        int nanoTail = Math.abs(now.getNano() / 1000) % 1000000;
        return "MPO" + ORDER_NO_TIME_FORMATTER.format(now)
                + String.format("%06d", nanoTail)
                + String.format("%04d", Integer.parseInt(seed));
    }
 
    /**
     * 订单插入后优先使用主键;极端情况下回查同一订单号,避免响应缺少订单编号。
     */
    private Long resolveOrderId(MemberPurchaseOrder order, Long userId, LocalDateTime now) {
        if (order.getId() != null) {
            return order.getId();
        }
        MemberPurchaseOrder latestOrder = memberPurchaseOrderMapper.selectOne(new LambdaQueryWrapper<MemberPurchaseOrder>()
                .eq(MemberPurchaseOrder::getAppUserId, userId)
                .eq(MemberPurchaseOrder::getOrderNo, order.getOrderNo())
                .eq(MemberPurchaseOrder::getDeleted, 0)
                .orderByDesc(MemberPurchaseOrder::getPaidAt)
                .orderByDesc(MemberPurchaseOrder::getId)
                .last("LIMIT 1"));
        if (latestOrder != null && latestOrder.getId() != null) {
            return latestOrder.getId();
        }
        return null;
    }
 
    /**
     * 将购买结果转换为 APP 可直接消费的响应体。
     */
    private AppMemberPurchaseResultVO buildPurchaseResult(MemberPurchaseOrder order, LocalDateTime memberExpireAt) {
        AppMemberPurchaseResultVO resultVO = new AppMemberPurchaseResultVO();
        resultVO.setOrderId(order.getId());
        resultVO.setOrderNo(order.getOrderNo());
        resultVO.setMemberType(order.getMemberType());
        resultVO.setMemberDays(order.getMemberDays());
        resultVO.setOrderAmount(order.getOrderAmount());
        resultVO.setPaymentMethod(order.getPaymentMethod());
        resultVO.setStatus(order.getStatus());
        resultVO.setPaidAt(order.getPaidAt());
        resultVO.setMemberExpireAt(memberExpireAt);
        return resultVO;
    }
 
    /**
     * 优先使用昵称快照,没有昵称时降级到微信昵称或用户编号。
     */
    private String resolveUserNickname(AppUser user) {
        String nickname = trimToEmpty(user.getNickname());
        if (!nickname.isEmpty()) {
            return nickname;
        }
        String wechatNickname = trimToEmpty(user.getWechatNickname());
        if (!wechatNickname.isEmpty()) {
            return wechatNickname;
        }
        return "用户" + user.getId();
    }
 
    /**
     * 将可空文本转换为去空格字符串。
     */
    private String trimToEmpty(String value) {
        if (value == null) {
            return "";
        }
        return value.trim();
    }
 
    /**
     * 会员套餐快照,避免业务逻辑反复透传零散字段。
     */
    private static final class PackageSnapshot {
 
        private final String memberType;
        private final Integer memberDays;
        private final BigDecimal orderAmount;
 
        /**
         * 创建会员套餐快照。
         */
        private PackageSnapshot(String memberType, Integer memberDays, BigDecimal orderAmount) {
            this.memberType = memberType;
            this.memberDays = memberDays;
            this.orderAmount = orderAmount;
        }
 
        /**
         * 获取会员套餐类型。
         */
        private String getMemberType() {
            return memberType;
        }
 
        /**
         * 获取会员有效天数。
         */
        private Integer getMemberDays() {
            return memberDays;
        }
 
        /**
         * 获取订单金额。
         */
        private BigDecimal getOrderAmount() {
            return orderAmount;
        }
    }
}