shinnosuke
2026-09-21 7844f5af457ff3b7d3f8eeac8799e8d281e635a1
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
package com.talentcard.server.business.admin;
 
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
 
import com.talentcard.server.business.PageResult;
import com.talentcard.server.common.ApiResponse;
import com.talentcard.server.request.TicketPackageCodeImportRequest;
import com.talentcard.server.request.TicketPackageCodeGenerateRequest;
import com.talentcard.server.request.TicketPackageCodeLockRequest;
import com.talentcard.server.request.TicketPackageCodeStatusRequest;
import com.talentcard.server.request.TicketPackageSupplementRequest;
import com.talentcard.server.request.TicketPackageSaveRequest;
import com.talentcard.server.request.TicketPackageStatusRequest;
import com.talentcard.server.service.TicketPackageAdminService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
 
@Tag(name = "后台-票包码池", description = "后台维护票包、兑换码码池、库存、发放记录和兑换码状态")
@RestController
@RequestMapping("/api/admin/ticket-packages")
public class TicketPackageAdminController {
    private final TicketPackageAdminService service;
 
    public TicketPackageAdminController(TicketPackageAdminService service) {
        this.service = service;
    }
 
    @Operation(summary = "查询票包列表", description = "分页查询票包配置和兑换码库存汇总")
    @GetMapping
    public ApiResponse<PageResult<Map<String, Object>>> packages(
            @Parameter(description = "指定兑换商家ID") @RequestParam(required = false) Long merchantId,
            @Parameter(description = "状态:1启用,0停用") @RequestParam(required = false) Integer status,
            @Parameter(description = "票包名称或编号关键词") @RequestParam(required = false) String keyword,
            @Parameter(description = "适用商家名称关键词") @RequestParam(required = false) String merchantKeyword,
            @Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
            @Parameter(description = "每页条数") @RequestParam(defaultValue = "10") int size
    ) {
        return ApiResponse.success(service.packages(merchantId, status, keyword, merchantKeyword, page, size));
    }
 
    @Operation(summary = "查询票包详情", description = "查看票包基础配置和码池库存汇总")
    @GetMapping("/{packageId}")
    public ApiResponse<Map<String, Object>> packageDetail(@PathVariable Long packageId) {
        return ApiResponse.success(service.packageDetail(packageId));
    }
 
    @Operation(summary = "票包适用商家选项", description = "票包表单的「适用商家」下拉,只返回启用的观影服务商家")
    @GetMapping("/merchant-options")
    public ApiResponse<List<Map<String, Object>>> merchantOptions(
            @Parameter(description = "商家名称关键词") @RequestParam(required = false) String keyword
    ) {
        return ApiResponse.success(service.merchantOptions(keyword));
    }
 
    @Operation(summary = "查询票包库存", description = "查看指定票包兑换码总量、可发放、锁定、已发放、已使用和作废数量")
    @GetMapping("/{packageId}/stock")
    public ApiResponse<Map<String, Object>> stock(@Parameter(description = "票包ID") @PathVariable Long packageId) {
        return ApiResponse.success(service.stock(packageId));
    }
 
    @Operation(summary = "新增票包", description = "后台录入票包,不涉及具体电影票场次")
    @PostMapping
    public ApiResponse<Map<String, Object>> createPackage(@Valid @RequestBody TicketPackageSaveRequest request) {
        return ApiResponse.success(service.createPackage(request));
    }
 
    @Operation(summary = "修改票包", description = "修改票包名称、适用卡等级、有效期、商家APP信息等")
    @PutMapping("/{packageId}")
    public ApiResponse<Map<String, Object>> updatePackage(
            @PathVariable Long packageId,
            @Valid @RequestBody TicketPackageSaveRequest request
    ) {
        return ApiResponse.success(service.updatePackage(packageId, request));
    }
 
    @Operation(summary = "修改票包状态", description = "启用或停用票包")
    @PatchMapping("/{packageId}/status")
    public ApiResponse<Map<String, Object>> updatePackageStatus(
            @PathVariable Long packageId,
            @Valid @RequestBody TicketPackageStatusRequest request
    ) {
        return ApiResponse.success(service.updatePackageStatus(packageId, request.status()));
    }
 
    @Operation(summary = "导入票包兑换码", description = "批量导入兑换码到指定票包码池,重复兑换码自动跳过")
    @PostMapping("/{packageId}/codes/import")
    public ApiResponse<Map<String, Object>> importCodes(
            @PathVariable Long packageId,
            @Valid @RequestBody TicketPackageCodeImportRequest request
    ) {
        return ApiResponse.success(service.importCodes(packageId, request));
    }
 
    @Operation(summary = "批量生成票包兑换码", description = "按数量自动生成兑换码并写入指定票包码池,重复兑换码自动跳过")
    @PostMapping("/{packageId}/codes/generate")
    public ApiResponse<Map<String, Object>> generateCodes(
            @PathVariable Long packageId,
            @Valid @RequestBody TicketPackageCodeGenerateRequest request
    ) {
        return ApiResponse.success(service.generateCodes(packageId, request));
    }
 
    @Operation(summary = "导入票包兑换码Excel", description = "上传Excel文件导入兑换码,读取第一个Sheet第一列,首行可为兑换码标题,重复兑换码自动跳过")
    @PostMapping(value = "/{packageId}/codes/import-file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ApiResponse<Map<String, Object>> importCodeFile(
            @Parameter(description = "票包ID") @PathVariable Long packageId,
            @Parameter(description = "兑换码Excel文件") @RequestParam("file") MultipartFile file,
            @Parameter(description = "导入批次号;不传后端自动生成") @RequestParam(required = false) String batchNo,
            @Parameter(description = "备注") @RequestParam(required = false) String remark
    ) {
        return ApiResponse.success(service.importCodeFile(packageId, file, batchNo, remark));
    }
 
    @Operation(summary = "查询票包兑换码列表", description = "分页查询码池兑换码,支持状态、批次号和关键词筛选")
    @GetMapping("/codes")
    public ApiResponse<PageResult<Map<String, Object>>> codes(
            @Parameter(description = "票包ID") @RequestParam(required = false) Long packageId,
            @Parameter(description = "兑换码状态") @RequestParam(required = false) String status,
            @Parameter(description = "导入批次号") @RequestParam(required = false) String batchNo,
            @Parameter(description = "兑换码或票包名称关键词") @RequestParam(required = false) String keyword,
            @Parameter(description = "票包名称或编号关键词") @RequestParam(required = false) String packageKeyword,
            @Parameter(description = "适用商家名称关键词") @RequestParam(required = false) String merchantKeyword,
            @Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
            @Parameter(description = "每页条数") @RequestParam(defaultValue = "10") int size
    ) {
        return ApiResponse.success(service.codes(packageId, status, batchNo, keyword, packageKeyword, merchantKeyword, page, size));
    }
 
    @Operation(summary = "修改兑换码状态", description = "后台作废兑换码,或将已发放兑换码标记为已使用")
    @PatchMapping("/codes/{codeId}/status")
    public ApiResponse<Map<String, Object>> updateCodeStatus(
            @PathVariable Long codeId,
            @Valid @RequestBody TicketPackageCodeStatusRequest request
    ) {
        return ApiResponse.success(service.updateCodeStatus(codeId, request));
    }
 
    @Operation(summary = "查询票包发放记录", description = "分页查询用户领取兑换码记录")
    @GetMapping("/claims")
    public ApiResponse<PageResult<Map<String, Object>>> claims(
            @Parameter(description = "票包ID") @RequestParam(required = false) Long packageId,
            @Parameter(description = "持卡人ID") @RequestParam(required = false) Long talentId,
            @Parameter(description = "发放状态") @RequestParam(required = false) String status,
            @Parameter(description = "领取编号或兑换码关键词") @RequestParam(required = false) String keyword,
            @Parameter(description = "票包名称或编号关键词") @RequestParam(required = false) String packageKeyword,
            @Parameter(description = "持卡人姓名、手机号或卡号关键词") @RequestParam(required = false) String talentKeyword,
            @Parameter(description = "卡片等级") @RequestParam(required = false) String cardLevel,
            @Parameter(description = "领取开始日期") @RequestParam(required = false) LocalDate claimedStart,
            @Parameter(description = "领取结束日期") @RequestParam(required = false) LocalDate claimedEnd,
            @Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
            @Parameter(description = "每页条数") @RequestParam(defaultValue = "10") int size
    ) {
        return ApiResponse.success(service.claims(packageId, talentId, status, keyword, packageKeyword, talentKeyword, cardLevel, claimedStart, claimedEnd, page, size));
    }
 
    @Operation(summary = "领取校验", description = "按兑换码码值校验兑换码是否可领取:是否存在、所属票包是否启用、是否在有效期内、是否适用于指定商户,以及当前状态(可用/锁定/已发放/已使用/已作废)")
    @GetMapping("/codes/validate")
    public ApiResponse<Map<String, Object>> validateCode(
            @Parameter(description = "兑换码码值", required = true) @RequestParam("code") String code,
            @Parameter(description = "票包ID;限定校验范围") @RequestParam(required = false) Long packageId,
            @Parameter(description = "适用商户ID;不匹配时返回不可领取") @RequestParam(required = false) Long merchantId
    ) {
        return ApiResponse.success(service.validateCode(code, packageId, merchantId));
    }
 
    @Operation(summary = "兑换码详情", description = "查看兑换码的码值、适用商户、有效期、库存状态、锁定信息和发放/领取状态(领取人、使用状态、使用时间)")
    @GetMapping("/codes/{codeId}")
    public ApiResponse<Map<String, Object>> codeDetail(
            @Parameter(description = "兑换码ID") @PathVariable Long codeId
    ) {
        return ApiResponse.success(service.codeDetail(codeId));
    }
 
    @Operation(summary = "发放锁定", description = "后台手动锁定一个可用兑换码并绑定领取人,用于线下发放前的占位预留;锁定后可补录发放、解锁或作废")
    @PostMapping("/codes/{codeId}/lock")
    public ApiResponse<Map<String, Object>> lockForIssue(
            @Parameter(description = "兑换码ID") @PathVariable Long codeId,
            @Valid @RequestBody TicketPackageCodeLockRequest request
    ) {
        return ApiResponse.success(service.lockForIssue(codeId, request));
    }
 
    @Operation(summary = "解锁兑换码", description = "将锁定状态的兑换码解锁回可用状态,并清除锁定领取人信息")
    @PatchMapping("/codes/{codeId}/release")
    public ApiResponse<Map<String, Object>> releaseLock(
            @Parameter(description = "兑换码ID") @PathVariable Long codeId
    ) {
        return ApiResponse.success(service.releaseLock(codeId));
    }
 
    @Operation(summary = "作废兑换码", description = "作废可用、锁定或已发放状态的兑换码;已发放的会同步作废其发放记录,已使用或已作废的兑换码不可作废")
    @PatchMapping("/codes/{codeId}/void")
    public ApiResponse<Map<String, Object>> voidCode(
            @Parameter(description = "兑换码ID") @PathVariable Long codeId,
            @Parameter(description = "作废备注") @RequestParam(required = false) String remark
    ) {
        return ApiResponse.success(service.voidCode(codeId, remark));
    }
 
    @Operation(summary = "补录发放记录", description = "线下已发放但系统未记录的兑换码,后台按兑换码ID或码值补录一条发放记录并将兑换码标记为已发放;重复补录同一兑换码会返回已有记录")
    @PostMapping("/claims/supplement")
    public ApiResponse<Map<String, Object>> supplementClaim(
            @Valid @RequestBody TicketPackageSupplementRequest request
    ) {
        return ApiResponse.success(service.supplementClaim(request));
    }
}