Administrator
2025-05-09 320e9165ac6cc6d90978fbef3074a8ed9add1790
后台管理页面完成
18个文件已修改
9个文件已添加
1183 ■■■■ 已修改文件
src/main/java/com/taoke/autopay/config/WebSecurityConfig.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/taoke/autopay/controller/CreditController.java 276 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/taoke/autopay/controller/WebApiController.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/taoke/autopay/controller/admin/credit/UserCreditExchangeRecordAdminController.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/taoke/autopay/controller/admin/credit/UserCreditRecordAdminController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/taoke/autopay/dao/credit/UserCreditRecordMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/taoke/autopay/dto/admin/CreditInfoDto.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/taoke/autopay/manager/UserCreditExchangeManager.java 101 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/taoke/autopay/service/impl/WxUserServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/taoke/autopay/service/impl/credit/UserCreditBalanceServiceImpl.java 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/taoke/autopay/utils/AlipayUtil.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/CreditExchangeRecordMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/UserCreditRecordMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/admin/credit-exchange-record-list.html 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/admin/credit-setting-list.html 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/agent/js/index.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/credit/alipay_account_setting.html 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/credit/credit_records.html 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/credit/exchange_records.html 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/credit/index.html 133 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/img/command.png 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/img/command_fill.png 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/img/credit.png 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/img/credit_fill.png 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/index3.html 67 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/js/http.js 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/taoke/autopay/config/WebSecurityConfig.java
@@ -198,7 +198,7 @@
        http.headers().frameOptions().disable();
        http.authorizeRequests()
                // 配置不需要鉴权的接口
                .antMatchers("/admin/api/captcha.jpg*", "/api/**", "/webapi/**","*/agentapi/**").permitAll()
                .antMatchers("/admin/api/captcha.jpg*", "/api/**", "/webapi/**","/credit/api/**","*/agentapi/**").permitAll()
                //配置需要鉴权的接口
                .antMatchers("/admin/api/**", "/admin/index.html").authenticated()
                .and()
src/main/java/com/taoke/autopay/controller/CreditController.java
New file
@@ -0,0 +1,276 @@
package com.taoke.autopay.controller;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.taoke.autopay.dao.credit.CreditExchangeRecordMapper;
import com.taoke.autopay.dao.credit.UserCreditRecordMapper;
import com.taoke.autopay.dto.admin.CreditInfoDto;
import com.taoke.autopay.entity.SystemConfigKeyEnum;
import com.taoke.autopay.entity.WxUserInfo;
import com.taoke.autopay.entity.credit.CreditExchangeRecord;
import com.taoke.autopay.entity.credit.UserAlipayBinding;
import com.taoke.autopay.entity.credit.UserCreditBalance;
import com.taoke.autopay.entity.credit.UserCreditRecord;
import com.taoke.autopay.exception.UserCreditExchangeException;
import com.taoke.autopay.manager.UserCreditExchangeManager;
import com.taoke.autopay.service.SystemConfigService;
import com.taoke.autopay.service.credit.CreditExchangeRecordService;
import com.taoke.autopay.service.credit.UserAlipayBindingService;
import com.taoke.autopay.service.credit.UserCreditBalanceService;
import com.taoke.autopay.service.credit.UserCreditRecordService;
import com.taoke.autopay.utils.Constant;
import com.taoke.autopay.utils.JsonUtil;
import com.taoke.autopay.utils.StringUtil;
import net.sf.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Date;
import java.util.List;
@RestController
@RequestMapping("/credit/api")
public class CreditController {
    private static final Logger logger = LoggerFactory.getLogger(CreditController.class);
    @Resource
    private UserCreditBalanceService userCreditBalanceService;
    @Resource
    private UserCreditExchangeManager userCreditExchangeManager;
    @Resource
    private CreditExchangeRecordService creditExchangeRecordService;
    @Resource
    private UserAlipayBindingService userAlipayBindingService;
    @Resource
    private UserCreditRecordService userCreditRecordService;
    @Resource
    private CreditExchangeRecordService userCreditExchangeRecordService;
    @Resource
    private SystemConfigService systemConfigService;
    private final Gson gson = new GsonBuilder().registerTypeAdapter(BigDecimal.class, new TypeAdapter<BigDecimal>() {
        @Override
        public void write(JsonWriter out, BigDecimal value) throws IOException {
            String desc = "";
            if (value != null) {
                out.value(value.setScale(2, RoundingMode.HALF_UP).toString());
            } else {
                out.value("");
            }
        }
        @Override
        public BigDecimal read(JsonReader in) throws IOException {
            return new BigDecimal("0");
        }
    }).create();
    private Long getUserId(HttpSession session) {
        WxUserInfo user = (WxUserInfo) session.getAttribute(Constant.SESSION_KEY_USER);
        if (user != null) {
            return user.getId();
        }
        return null;
    }
    private String getLoginLinkContent() {
        String redictLink = systemConfigService.getValueCache(SystemConfigKeyEnum.WX_REDIRECT_LINK);
        redictLink = redictLink.replace("snsapi_base", "snsapi_userinfo");
        // 没有登录,返回登录链接
        JSONObject root = new JSONObject();
        root.put("link", redictLink);
        return JsonUtil.loadTrueResult(Constant.RESULT_CODE_NEED_LOGIN, root);
    }
    /**
     * 获取积分信息接口
     * 返回积分余额,剩余积分可兑换的红包,正在兑换中的积分数量
     */
    @RequestMapping("/info")
    public String getCreditInfo(HttpSession session) {
        Long userId = getUserId(session);
        if (userId == null) {
            return getLoginLinkContent();
        }
        int balanceAmount = 0;
        UserCreditBalance balance = userCreditBalanceService.getCreditBalanceByUserId(userId);
        if (balance != null) {
            balanceAmount = balance.getCreditBalance();
        }
        BigDecimal money = new BigDecimal(0);
        try {
            money = userCreditExchangeManager.calculateExchangeAmount(userId, balanceAmount, false);
        } catch (UserCreditExchangeException e) {
            throw new RuntimeException(e);
        }
        List<CreditExchangeRecord> recordList = creditExchangeRecordService.listExchangeRecords(CreditExchangeRecordMapper.DaoQuery.builder()
                .uid(userId)
                .exchangeStatus(CreditExchangeRecord.STATUS_NOT_VERIFY)
                .count(100)
                .build());
        int exchaningCredits = 0;
        if (recordList != null) {
            for (CreditExchangeRecord record : recordList) {
                exchaningCredits += record.getConsumedCredits();
            }
        }
        try {
            return JsonUtil.loadTrueResult(gson.toJson(CreditInfoDto.builder()
                    .balance(balanceAmount)
                    .exchangeMoney(money)
                    .exchangingCredits(exchaningCredits)
                    .build()));
        } catch (Exception e) {
            logger.error("获取积分信息失败", e);
            return JsonUtil.loadFalseResult("获取积分信息失败");
        }
    }
    /**
     * 获取支付宝绑定信息
     */
    @RequestMapping("/getAlipayBinding")
    public String getAlipayBinding(HttpSession session) {
        Long uid = getUserId(session);
        if (uid == null) {
            return getLoginLinkContent();
        }
        List<UserAlipayBinding> alipayBindings = userAlipayBindingService.getBindingsByUid(uid);
        if (alipayBindings == null || alipayBindings.isEmpty()) {
            return JsonUtil.loadFalseResult("用户未绑定支付宝账户");
        }
        try {
            return JsonUtil.loadTrueResult(alipayBindings.get(0));
        } catch (Exception e) {
            logger.error("获取支付宝绑定信息失败", e);
            return JsonUtil.loadFalseResult("获取支付宝绑定信息失败");
        }
    }
    /**
     * 修改支付宝绑定信息
     */
    @PostMapping("/updateAlipayBinding")
    public String updateAlipayBinding(String alipayName, String alipayAccount, HttpSession session) {
        Long userId = getUserId(session);
        if (userId == null) {
            return getLoginLinkContent();
        }
        List<UserAlipayBinding> alipayBindings = userAlipayBindingService.getBindingsByUid(userId);
        if (alipayBindings != null && !alipayBindings.isEmpty()) {
            userAlipayBindingService.updateBinding(UserAlipayBinding.builder()
                    .id(alipayBindings.get(0).getId())
                    .alipayName(alipayName)
                    .alipayAccount(alipayAccount)
                    .updateTime(new Date())
                    .build());
        } else {
            userAlipayBindingService.addBinding(UserAlipayBinding.builder()
                    .uid(userId)
                    .alipayName(alipayName)
                    .alipayAccount(alipayAccount)
                    .createTime(new Date())
                    .build());
        }
        return JsonUtil.loadTrueResult("修改成功");
    }
    /**
     * 获取积分记录接口(可分页)
     */
    @RequestMapping("/records")
    public String getCreditRecords(@RequestParam(defaultValue = "1") int page,
                                   @RequestParam(defaultValue = "10") int pageSize,
                                   HttpSession session) {
        Long userId = getUserId(session);
        if (userId == null) {
            return getLoginLinkContent();
        }
        UserCreditRecordMapper.DaoQuery query = UserCreditRecordMapper.DaoQuery.builder()
                .uid(userId)
                .start((long) (page - 1) * pageSize)
                .count(pageSize)
                .build();
        List<UserCreditRecord> recordList = userCreditRecordService.listCreditRecords(query);
        long count = userCreditRecordService.countCreditRecords(query);
        JSONObject root = new JSONObject();
        root.put("list", gson.toJson(recordList));
        root.put("count", count);
        return JsonUtil.loadTrueResult(root);
    }
    /**
     * 积分兑换接口
     */
    @PostMapping("/exchange")
    public String exchangeCredits(@RequestParam int credits, HttpSession session) {
        Long userId = getUserId(session);
        if (userId == null) {
            return getLoginLinkContent();
        }
        try {
            userCreditExchangeManager.exchangeCredit(CreditExchangeRecord.builder()
                    .consumedCredits(credits)
                    .uid(userId)
                    .exchangeType(CreditExchangeRecord.ExchangeType.FUND_EXCHANGE)
                    .createTime(new Date())
                    .build());
            return JsonUtil.loadTrueResult("兑换成功");
        } catch (UserCreditExchangeException e) {
            logger.error("积分兑换失败", e);
            return JsonUtil.loadFalseResult(e.getMessage());
        } catch (Exception e) {
            logger.error("积分兑换失败", e);
            return JsonUtil.loadFalseResult("兑换失败,请稍后重试");
        }
    }
    /**
     * 积分兑换接口(可分页)
     */
    @PostMapping("/exchange_records")
    public String exchangeCredits(
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int pageSize,
            HttpSession session) {
        Long userId = getUserId(session);
        if (userId == null) {
            return getLoginLinkContent();
        }
        CreditExchangeRecordMapper.DaoQuery query = CreditExchangeRecordMapper.DaoQuery.builder()
                .uid(userId)
                .start((long) (page - 1) * pageSize)
                .count(pageSize)
                .build();
        List<CreditExchangeRecord> recordList = userCreditExchangeRecordService.listExchangeRecords(query);
        long count = userCreditExchangeRecordService.countExchangeRecords(query);
        JSONObject root = new JSONObject();
        root.put("list", gson.toJson(recordList));
        root.put("count", count);
        return JsonUtil.loadTrueResult(root);
    }
}
src/main/java/com/taoke/autopay/controller/WebApiController.java
@@ -20,7 +20,6 @@
import net.sf.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -32,7 +31,10 @@
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.*;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller
@RequestMapping("webapi")
@@ -238,16 +240,43 @@
    }
    private WxUserInfo wxLogin(String code, HttpSession session) throws Exception {
        WXAppInfoDto wxApp = systemConfigService.getWxAppInfoCache();
        WxApiUtil.WXAccessTokenInfo tokenInfo = WxApiUtil.getAcessTokenInfo(code, wxApp);
        if (tokenInfo != null && !StringUtil.isNullOrEmpty(tokenInfo.getOpenid())) {
            WxApiUtil.WXUserInfo wxUserInfo = null;
            if (tokenInfo.getScope() != null && tokenInfo.getScope().contains("snsapi_userinfo")) {
                try {
                    wxUserInfo = WxApiUtil.getUserInfo(tokenInfo.getAccess_token(), tokenInfo.getOpenid());
                    wxLogger.info("解析结果", new Gson().toJson(wxUserInfo));
                } catch (Exception e) {
                    wxLogger.error("解析出错", e);
                }
            }
            if (wxUserInfo == null) {
                wxUserInfo = new WxApiUtil.WXUserInfo();
                wxUserInfo.setOpenid(tokenInfo.getOpenid());
            }
            WxUserInfo user = wxUserService.login(wxUserInfo);
            session.setAttribute(Constant.SESSION_KEY_USER, user);
            wxLogger.info("微信保存用户信息:{} id-{}", session.getId(), user.getId());
            return user;
        }
       throw new Exception("获取授权信息异常");
    }
    @RequestMapping(value = "wxLogin")
    public void wxLogin(String code, String state, HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
    public void wxLogin(String code, String state, HttpServletRequest request, HttpServletResponse
            response, HttpSession session) throws IOException {
        // 根据code获取openid
        SubmitKeyInfo alipayKeyInfo = (SubmitKeyInfo) session.getAttribute(Constant.SESSION_KEY_TEMP_ALIPAY_KEY);
        wxLogger.info("微信授权回调:{} code-{} referer-{}", session.getId(), code, alipayKeyInfo.getReferer());
        if (alipayKeyInfo != null) {
            wxLogger.info("微信授权回调:{} code-{} referer-{}", session.getId(), code, alipayKeyInfo.getReferer());
        }
        String failLink = systemConfigService.getValueCache(SystemConfigKeyEnum.WX_LOGIN_FAIL_LINK);
        String referer = alipayKeyInfo.getReferer();
        String referer = alipayKeyInfo != null ? alipayKeyInfo.getReferer() : "";
        try {
            WXAppInfoDto wxApp = systemConfigService.getWxAppInfoCache();
            String successLink = systemConfigService.getValueCache(SystemConfigKeyEnum.WX_LOGIN_SUCCESS_LINK);
            if (!StringUtil.isNullOrEmpty(referer)) {
                Map<String, String> params = HttpUtil.getPramsFromUrl(referer);
@@ -255,41 +284,31 @@
                successLink = HttpUtil.getWholeUrl(HttpUtil.getUrlWithoutParams(referer), params);
            }
            WxApiUtil.WXAccessTokenInfo tokenInfo = WxApiUtil.getAcessTokenInfo(code, wxApp);
            if (tokenInfo != null && !StringUtil.isNullOrEmpty(tokenInfo.getOpenid())) {
                WxApiUtil.WXUserInfo wxUserInfo = null;
                if (tokenInfo.getScope() != null && tokenInfo.getScope().contains("snsapi_userinfo")) {
                    try {
                        wxUserInfo = WxApiUtil.getUserInfo(tokenInfo.getAccess_token(), tokenInfo.getOpenid());
                        wxLogger.info("解析结果", new Gson().toJson(wxUserInfo));
                    } catch (Exception e) {
                        wxLogger.error("解析出错", e);
                    }
                }
                if (wxUserInfo == null) {
                    wxUserInfo = new WxApiUtil.WXUserInfo();
                    wxUserInfo.setOpenid(tokenInfo.getOpenid());
                }
                WxUserInfo user = wxUserService.login(wxUserInfo);
                session.setAttribute(Constant.SESSION_KEY_USER, user);
                wxLogger.info("微信保存用户信息:{} id-{}", session.getId(), user.getId());
                wxLogger.info("从session读取到key:{}", alipayKeyInfo);
                if (alipayKeyInfo != null) {
                    if (!ipInfoMap.containsKey(alipayKeyInfo.getIp())) {
                        try {
                            IPUtil.IPInfo ipInfo = IPUtil.getLocalIPInfo(alipayKeyInfo.getIp());
                            ipInfoMap.put(alipayKeyInfo.getIp(), ipInfo);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                    addKey(alipayKeyInfo, user.getId());
                }
                response.sendRedirect(successLink);
            WxUserInfo user = wxLogin(code, session);
            if(alipayKeyInfo==null){
                // 普通登录
                wxLogger.info("普通登录成功");
                response.sendRedirect("/credit/index.html?state=SUCCESS");
                return;
            }
        } catch (Exception e) {
            wxLogger.info("从session读取到key:{}", alipayKeyInfo);
            if (alipayKeyInfo != null) {
                if (!ipInfoMap.containsKey(alipayKeyInfo.getIp())) {
                    try {
                        IPUtil.IPInfo ipInfo = IPUtil.getLocalIPInfo(alipayKeyInfo.getIp());
                        ipInfoMap.put(alipayKeyInfo.getIp(), ipInfo);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
                addKey(alipayKeyInfo, user.getId());
            }
            response.sendRedirect(successLink);
            return;
        } catch (
                Exception e) {
            wxLogger.error("授权失败:{}", e.getMessage());
            if (!StringUtil.isNullOrEmpty(referer)) {
                Map<String, String> params = HttpUtil.getPramsFromUrl(referer);
@@ -360,4 +379,5 @@
        return JsonUtil.loadTrueResult(new Gson().toJson(map));
    }
}
src/main/java/com/taoke/autopay/controller/admin/credit/UserCreditExchangeRecordAdminController.java
@@ -22,6 +22,7 @@
import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@@ -92,7 +93,7 @@
        if (!StringUtil.isNullOrEmpty(search.getEnd_date())) {
            query.maxCreateTime = new Date(TimeUtil.convertToTimeTemp(search.getEnd_date(), "yyyy-MM-dd") + 1000 * 60 * 60 * 24L);
        }
        query.sortList = Arrays.asList(new String[]{"_create_time desc"});
        query.sortList = Arrays.asList(new String[]{"create_time desc"});
        query.start = (long) (page - 1) * limit;
        query.count = limit;
@@ -114,16 +115,31 @@
        int successCount = 0;
        int failCount = 0;
        JSONArray idsArray = JSONArray.fromObject(ids);
        List<String> errorMsgList=new ArrayList<>();
        for (int i = 0; i < idsArray.size(); i++) {
            long id = idsArray.optLong(i);
            try {
                userCreditExchangeManager.approveExchange(id);
                successCount += 1;
            } catch (UserCreditExchangeException e) {
                errorMsgList.add(e.getMessage());
                failCount += 1;
                userCreditExchangeRecordService.updateExchangeRecord(CreditExchangeRecord.builder()
                        .id(id)
                        .exchangeStatusDescription(e.getMessage())
                        .updateTime(new Date())
                        .build());
            }
        }
        return JsonUtil.loadFalseResult(String.format("成功通过 %d 条记录 通过异常 %d 条记录", successCount, failCount));
        if(idsArray.size()>1) {
            return JsonUtil.loadFalseResult(String.format("成功通过 %d 条记录 通过异常 %d 条记录", successCount, failCount));
        }else{
            if(!errorMsgList.isEmpty()) {
                return JsonUtil.loadFalseResult(errorMsgList.get(0));
            }else{
                return JsonUtil.loadTrueResult("通过成功");
            }
        }
    }
    /**
src/main/java/com/taoke/autopay/controller/admin/credit/UserCreditRecordAdminController.java
@@ -71,7 +71,7 @@
    @ResponseBody
    @RequestMapping("list")
    public String listRecords(UserCreditRecordSearchVO search, int page, int limit) {
        UserCreditRecordMapper.DaoQuery query = new UserCreditRecordMapper.DaoQuery();
        UserCreditRecordMapper.DaoQuery query =  UserCreditRecordMapper.DaoQuery.builder().build();
        if(!StringUtil.isNullOrEmpty(search.getUid())){
            query.uid = Long.parseLong(search.getUid());
        }
@@ -86,7 +86,7 @@
        if(!StringUtil.isNullOrEmpty(search.getEnd_date())){
            query.maxCreateTime = new Date(TimeUtil.convertToTimeTemp(search.getEnd_date(),"yyyy-MM-dd")+1000*60*60*24L);
        }
        query.sortList = Arrays.asList(new String[]{ "_create_time desc"});
        query.sortList = Arrays.asList(new String[]{ "create_time desc"});
        query.start = (long) (page - 1) * limit;
        query.count = limit;
src/main/java/com/taoke/autopay/dao/credit/UserCreditRecordMapper.java
@@ -1,6 +1,8 @@
package com.taoke.autopay.dao.credit;
import java.util.Date;
import lombok.Builder;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import com.taoke.autopay.entity.credit.UserCreditRecord;
@@ -16,6 +18,7 @@
    long count(@Param("query") DaoQuery query);
    @Builder
    public static class DaoQuery {
        public Long id;
        public Long uid;
src/main/java/com/taoke/autopay/dto/admin/CreditInfoDto.java
New file
@@ -0,0 +1,15 @@
package com.taoke.autopay.dto.admin;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Builder
public class CreditInfoDto {
   private int balance;
   private BigDecimal exchangeMoney;
   private int exchangingCredits;
}
src/main/java/com/taoke/autopay/manager/UserCreditExchangeManager.java
@@ -51,16 +51,15 @@
    public Long exchangeCredit(CreditExchangeRecord request) throws UserCreditExchangeException {
        Long userId = request.getUid();
        CreditExchangeRecord.ExchangeType exchangeType = request.getExchangeType();
        if(exchangeType!=CreditExchangeRecord.ExchangeType.FUND_EXCHANGE){
            throw  new UserCreditExchangeException(UserCreditExchangeException.CODE_COMMON,"只支持红包兑换");
        if (exchangeType != CreditExchangeRecord.ExchangeType.FUND_EXCHANGE) {
            throw new UserCreditExchangeException(UserCreditExchangeException.CODE_COMMON, "只支持红包兑换");
        }
        // 检查用户积分余额是否足够
        UserCreditBalance balance = userCreditBalanceService.getCreditBalanceByUserId(userId);
        if (balance == null || balance.getCreditBalance() <request.getConsumedCredits()) {
        if (balance == null || balance.getCreditBalance() < request.getConsumedCredits()) {
            throw new UserCreditExchangeException(UserCreditExchangeException.CODE_BALANCE_NOT_ENOUGH, "用户积分不足");
        }
        // 如果是红包兑换,判断用户兑换频率
@@ -70,7 +69,7 @@
        // 查询用户绑定的支付宝账户
        List<UserAlipayBinding> alipayBindings = userAlipayBindingService.getBindingsByUid(userId);
        if (alipayBindings == null|| alipayBindings.isEmpty()) {
        if (alipayBindings == null || alipayBindings.isEmpty()) {
            throw new UserCreditExchangeException(UserCreditExchangeException.CODE_NOT_BIND_ALIPAY_ACCOUNT, "用户未绑定支付宝账户");
        }
@@ -78,7 +77,7 @@
        if (exchangeType == CreditExchangeRecord.ExchangeType.FUND_EXCHANGE) {
            // 计算可兑换金额(如果是红包兑换)
            BigDecimal exchangeAmount = calculateExchangeAmount(request.getUid(), request.getConsumedCredits());
            BigDecimal exchangeAmount = calculateExchangeAmount(request.getUid(), request.getConsumedCredits(), true);
            exchangeRecord.setExchangeValue(exchangeAmount);
        }
@@ -86,7 +85,7 @@
        exchangeRecord.setUid(userId);
        exchangeRecord.setExchangeType(exchangeType);
        exchangeRecord.setConsumedCredits(request.getConsumedCredits());
        exchangeRecord.setCreditBalance(balance.getCreditBalance()-request.getConsumedCredits());
        exchangeRecord.setCreditBalance(balance.getCreditBalance() - request.getConsumedCredits());
        exchangeRecord.setExchangeStatus(CreditExchangeRecord.STATUS_NOT_VERIFY);
        exchangeRecord.setExchangeStatusDescription("未审核");
        exchangeRecord.setExchangeInfo1(alipayBindings.get(0).getAlipayName()); // 设置支付宝账户
@@ -100,7 +99,7 @@
                .creditAmount(request.getConsumedCredits())
                .consumptionMethod(UserCreditRecord.ConsumptionMethod.EXCHANGE_RED_PACKET)
                .direction(UserCreditRecord.DIRECTION_CONSUME)
                .identifierId(exchangeRecord.getId()+"")
                .identifierId(exchangeRecord.getId() + "")
                .uid(exchangeRecord.getUid())
                .description("红包兑换")
                .build());
@@ -113,22 +112,23 @@
     * @param exchangeRecordId 兑换记录ID
     */
    @Transactional(rollbackFor = Exception.class)
    public void approveExchange(Long exchangeRecordId) throws UserCreditExchangeException{
    public void approveExchange(Long exchangeRecordId) throws UserCreditExchangeException {
        CreditExchangeRecord exchangeRecord = userCreditExchangeRecordService.getExchangeRecordByIdForUpdate(exchangeRecordId);
        if (exchangeRecord == null) {
            throw new UserCreditExchangeException(UserCreditExchangeException.CODE_NOT_BIND_ALIPAY_ACCOUNT, "兑换记录不存在");
        }
        if(exchangeRecord.getExchangeStatus() != CreditExchangeRecord.STATUS_NOT_VERIFY){
        if (exchangeRecord.getExchangeStatus() != CreditExchangeRecord.STATUS_NOT_VERIFY) {
            throw new UserCreditExchangeException(UserCreditExchangeException.CODE_COMMON, "兑换已处理");
        }
        // 如果是红包兑换,调用通过兑换逻辑(TODO)
        if (exchangeRecord.getExchangeType() == CreditExchangeRecord.ExchangeType.FUND_EXCHANGE) {
            try {
                AlipayUtil.transfer("credit_exchange_"+exchangeRecordId, exchangeRecord.getExchangeInfo2(), exchangeRecord.getExchangeInfo1(), exchangeRecord.getExchangeValue(), "红包兑换", "红包兑换");
                AlipayUtil.transfer("credit_exchange_" + exchangeRecordId, exchangeRecord.getExchangeInfo2(), exchangeRecord.getExchangeInfo1(), exchangeRecord.getExchangeValue(), "红包兑换", "红包兑换");
            } catch (AlipayApiException e) {
                throw new UserCreditExchangeException(UserCreditExchangeException.CODE_ALIPAY_TRANSFER_FAILED, e.getErrCode()+ ":"+ e.getErrMsg());
                throw new UserCreditExchangeException(UserCreditExchangeException.CODE_ALIPAY_TRANSFER_FAILED, e.getErrCode() + ":" + e.getErrMsg());
            } catch (AlipayUtil.AlipayTransferException e) {
                throw new UserCreditExchangeException(UserCreditExchangeException.CODE_ALIPAY_TRANSFER_FAILED, e.getMessage());
            }
@@ -136,11 +136,12 @@
        // 改变兑换记录状态
        userCreditExchangeRecordService.updateExchangeRecord(CreditExchangeRecord.builder()
                        .id(exchangeRecordId)
                        .exchangeStatus(CreditExchangeRecord.STATUS_PASSED)
                        .exchangeStatusDescription("兑换已通过")
                        .updateTime(new Date())
                .id(exchangeRecordId)
                .exchangeStatus(CreditExchangeRecord.STATUS_PASSED)
                .exchangeStatusDescription("兑换已通过")
                .updateTime(new Date())
                .build());
    }
    /**
@@ -149,13 +150,13 @@
     * @param exchangeRecordId 兑换记录ID
     */
    @Transactional(rollbackFor = Exception.class)
    public void rejectExchange(Long exchangeRecordId) throws UserCreditExchangeException{
    public void rejectExchange(Long exchangeRecordId) throws UserCreditExchangeException {
        CreditExchangeRecord exchangeRecord = userCreditExchangeRecordService.getExchangeRecordByIdForUpdate(exchangeRecordId);
        if (exchangeRecord == null) {
            throw new UserCreditExchangeException(UserCreditExchangeException.CODE_NOT_BIND_ALIPAY_ACCOUNT, "兑换记录不存在");
        }
        if(exchangeRecord.getExchangeStatus() != CreditExchangeRecord.STATUS_NOT_VERIFY){
        if (exchangeRecord.getExchangeStatus() != CreditExchangeRecord.STATUS_NOT_VERIFY) {
            throw new UserCreditExchangeException(UserCreditExchangeException.CODE_COMMON, "兑换已处理");
        }
@@ -170,7 +171,7 @@
                .identifierId(exchangeRecord.getId().toString())
                .acquisitionMethod(UserCreditRecord.AcquisitionMethod.EXCHANGE_RETURN)
                .description("兑换退回")
               .createTime(new Date())
                .createTime(new Date())
                .updateTime(new Date()).build());
        userCreditExchangeRecordService.updateExchangeRecord(CreditExchangeRecord.builder()
                .id(exchangeRecordId)
@@ -185,65 +186,67 @@
     *
     * @param userId 用户ID
     */
    private void checkRedPacketExchangeFrequency(Long userId) throws UserCreditExchangeException{
    private void checkRedPacketExchangeFrequency(Long userId) throws UserCreditExchangeException {
        //实现红包兑换频率检查逻辑
        // 获取今日兑换的次数
        long nowTimeStamp=System.currentTimeMillis();
        long count =  userCreditExchangeRecordService.countExchangeRecords(CreditExchangeRecordMapper.DaoQuery.builder()
                        .uid(userId)
                        .minCreateTime(new Date(TimeUtil.convertToTimeTemp(TimeUtil.getGernalTime(nowTimeStamp,"yyyyMMdd"),"yyyyMMdd")))
                        .maxCreateTime(new Date(nowTimeStamp))
        long nowTimeStamp = System.currentTimeMillis();
        long count = userCreditExchangeRecordService.countExchangeRecords(CreditExchangeRecordMapper.DaoQuery.builder()
                .uid(userId)
                .minCreateTime(new Date(TimeUtil.convertToTimeTemp(TimeUtil.getGernalTime(nowTimeStamp, "yyyyMMdd"), "yyyyMMdd")))
                .maxCreateTime(new Date(nowTimeStamp))
                .build());
        CreditSetting setting =  creditSettingService.getSettingCacheByType(CreditSetting.CreditSettingType.DAILY_EXCHANGE_LIMIT, new Date(TimeUtil.convertToTimeTemp(TimeUtil.getGernalTime(nowTimeStamp,"yyyyMMddHHmm"),"yyyyMMddHHmm")));
        if(setting==null){
            throw new UserCreditExchangeException(UserCreditExchangeException.CODE_EXCHANGE_FREQUENCY_LIMIT,"兑换频率未设置");
        CreditSetting setting = creditSettingService.getSettingCacheByType(CreditSetting.CreditSettingType.DAILY_EXCHANGE_LIMIT, new Date(TimeUtil.convertToTimeTemp(TimeUtil.getGernalTime(nowTimeStamp, "yyyyMMddHHmm"), "yyyyMMddHHmm")));
        if (setting == null) {
            throw new UserCreditExchangeException(UserCreditExchangeException.CODE_EXCHANGE_FREQUENCY_LIMIT, "兑换频率未设置");
        }
        if(count>=Integer.parseInt(setting.getValue())){
        if (count >= Integer.parseInt(setting.getValue())) {
            throw new UserCreditExchangeException(UserCreditExchangeException.CODE_EXCHANGE_FREQUENCY_LIMIT, String.format("每天只能兑换%s次", setting.getValue()));
        }
    }
    /**
     * 计算兑换金额
     *
     * @param uid
     * @param credit
     * @return
     */
    public BigDecimal calculateExchangeAmount(Long uid, int credit) throws UserCreditExchangeException{
        long count =  userCreditExchangeRecordService.countExchangeRecords(CreditExchangeRecordMapper.DaoQuery.builder()
    public BigDecimal calculateExchangeAmount(Long uid, int credit, boolean forExchange) throws UserCreditExchangeException {
        long count = userCreditExchangeRecordService.countExchangeRecords(CreditExchangeRecordMapper.DaoQuery.builder()
                .uid(uid).build());
        Date nowDate =  new Date(TimeUtil.convertToTimeTemp(TimeUtil.getGernalTime(System.currentTimeMillis(),"yyyyMMddHHmm"),"yyyyMMddHHmm"));
        Date nowDate = new Date(TimeUtil.convertToTimeTemp(TimeUtil.getGernalTime(System.currentTimeMillis(), "yyyyMMddHHmm"), "yyyyMMddHHmm"));
        BigDecimal money =null;
        if(count<=0){
        BigDecimal money = null;
        if (count <= 0) {
            // 新人兑换
            List<ExchangeRate>  rates =  exchangeRateService.listExchangeRates(ExchangeRateMapper.DaoQuery.builder()
                            .exchangeType(ExchangeRate.ExchangeRateType.NEW_USER_EXCHANGE)
                            .maxStartTime(nowDate)
                            .minEndTime(nowDate)
                            .count(Integer.MAX_VALUE)
            List<ExchangeRate> rates = exchangeRateService.listExchangeRates(ExchangeRateMapper.DaoQuery.builder()
                    .exchangeType(ExchangeRate.ExchangeRateType.NEW_USER_EXCHANGE)
                    .maxStartTime(nowDate)
                    .minEndTime(nowDate)
                    .count(Integer.MAX_VALUE)
                    .build());
            if(rates.isEmpty()) {
                throw new UserCreditExchangeException(UserCreditExchangeException.CODE_COMMON,"新人兑换汇率未设置");
            if (rates.isEmpty()) {
                throw new UserCreditExchangeException(UserCreditExchangeException.CODE_COMMON, "新人兑换汇率未设置");
            }
            money = new BigDecimal(credit).multiply(rates.get(0).getRate()).setScale(2, RoundingMode.HALF_UP);
        }else{
        } else {
            // 老用户兑换
            List<ExchangeRate>  rates =  exchangeRateService.listExchangeRates(ExchangeRateMapper.DaoQuery.builder()
            List<ExchangeRate> rates = exchangeRateService.listExchangeRates(ExchangeRateMapper.DaoQuery.builder()
                    .exchangeType(ExchangeRate.ExchangeRateType.GENERAL_EXCHANGE)
                    .maxStartTime(nowDate)
                    .minEndTime(nowDate)
                    .count(Integer.MAX_VALUE)
                    .build());
            if(rates.isEmpty()) {
                throw new UserCreditExchangeException(UserCreditExchangeException.CODE_COMMON,"老用户兑换汇率未设置");
            if (rates.isEmpty()) {
                throw new UserCreditExchangeException(UserCreditExchangeException.CODE_COMMON, "老用户兑换汇率未设置");
            }
            money = new BigDecimal(credit).multiply(rates.get(0).getRate()).setScale(2, RoundingMode.HALF_UP);
        }
        CreditSetting setting =  creditSettingService.getSettingCacheByType(CreditSetting.CreditSettingType.MINIMUM_EXCHANGE_AMOUNT, nowDate);
        if(setting!=null&& new BigDecimal(setting.getValue()).compareTo(money)>0){
            throw new UserCreditExchangeException(UserCreditExchangeException.CODE_COMMON,String.format("兑换金额不能低于%s元",setting.getValue()));
        if (forExchange) {
            CreditSetting setting = creditSettingService.getSettingCacheByType(CreditSetting.CreditSettingType.MINIMUM_EXCHANGE_AMOUNT, nowDate);
            if (setting != null && new BigDecimal(setting.getValue()).compareTo(money) > 0) {
                throw new UserCreditExchangeException(UserCreditExchangeException.CODE_COMMON, String.format("兑换金额不能低于%s元", setting.getValue()));
            }
        }
        return money;
    }
src/main/java/com/taoke/autopay/service/impl/WxUserServiceImpl.java
@@ -8,6 +8,7 @@
import com.taoke.autopay.service.SystemConfigService;
import com.taoke.autopay.service.WxUserService;
import com.taoke.autopay.service.WxUserSettingService;
import com.taoke.autopay.service.credit.UserCreditBalanceService;
import com.taoke.autopay.utils.IPUtil;
import com.taoke.autopay.utils.StringUtil;
import com.taoke.autopay.utils.WxApiUtil;
@@ -36,6 +37,9 @@
    @Resource
    private SystemConfigService systemConfigService;
    @Resource
    private UserCreditBalanceService userCreditBalanceService;
    @Transactional(rollbackFor = Exception.class)
    @Override
@@ -55,6 +59,7 @@
            user.setLoginTime(new Date());
            user.setCreateTime(new Date());
            wxUserInfoMapper.insertSelective(user);
            userCreditBalanceService.initializeCreditBalance(user.getId());
            return user;
        } else {
            WxUserInfo update = new WxUserInfo();
@@ -64,6 +69,7 @@
            update.setLoginTime(new Date());
            update.setUpdateTime(new Date());
            wxUserInfoMapper.updateByPrimaryKeySelective(update);
            userCreditBalanceService.initializeCreditBalance(update.getId());
            return list.get(0);
        }
    }
src/main/java/com/taoke/autopay/service/impl/credit/UserCreditBalanceServiceImpl.java
@@ -22,7 +22,7 @@
    @Override
    public void initializeCreditBalance(Long userId) {
        if(getCreditBalanceByUserId(userId)!=null){
        if (getCreditBalanceByUserId(userId) != null) {
            return;
        }
        UserCreditBalance userCreditBalance = new UserCreditBalance();
@@ -30,7 +30,7 @@
        userCreditBalance.setCreditBalance(0);
        userCreditBalance.setCreateTime(new Date());
        userCreditBalance.setUpdateTime(new Date());
        userCreditBalanceMapper.insert(userCreditBalance);
        userCreditBalanceMapper.insertSelective(userCreditBalance);
    }
    @Override
@@ -52,18 +52,28 @@
    @Override
    public void increaseCreditBalance(Long userId, int amount) {
        UserCreditBalance userCreditBalance = userCreditBalanceMapper.selectByPrimaryKeyForUpdate(userId);
        if (userCreditBalance == null) {
            initializeCreditBalance(userId);
            userCreditBalance = userCreditBalanceMapper.selectByPrimaryKeyForUpdate(userId);
        }
        if (userCreditBalance != null) {
            userCreditBalanceMapper.updateByPrimaryKeySelective(UserCreditBalance.builder()
                            .id(userCreditBalance.getId())
                            .creditBalance(userCreditBalance.getCreditBalance() + amount)
                            .updateTime(new Date())
                    .id(userCreditBalance.getId())
                    .creditBalance(userCreditBalance.getCreditBalance() + amount)
                    .updateTime(new Date())
                    .build());
        }
    }
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void decreaseCreditBalance(Long userId, int amount) {
        UserCreditBalance userCreditBalance = userCreditBalanceMapper.selectByPrimaryKeyForUpdate(userId);
        if (userCreditBalance == null) {
            initializeCreditBalance(userId);
            userCreditBalance = userCreditBalanceMapper.selectByPrimaryKeyForUpdate(userId);
        }
        if (userCreditBalance != null) {
            userCreditBalanceMapper.updateByPrimaryKeySelective(UserCreditBalance.builder()
                    .id(userCreditBalance.getId())
src/main/java/com/taoke/autopay/utils/AlipayUtil.java
@@ -11,10 +11,12 @@
import com.alipay.api.response.AlipayFundTransUniTransferResponse;
import net.sf.json.JSONObject;
import org.slf4j.Logger;
public class AlipayUtil {
    private final static Logger logger = org.slf4j.LoggerFactory.getLogger("debugLogger");
    private static DefaultAlipayClient alipayClient = null;
@@ -37,6 +39,7 @@
            alipayClient = new DefaultAlipayClient(certAlipayRequest);
        } catch (AlipayApiException e) {
            e.printStackTrace();
            logger.error("支付宝初始化失败", e);
        }
    }
src/main/resources/application.yml
@@ -1,3 +1,3 @@
spring:
  profiles:
    active: dev
    active: pro
src/main/resources/mapper/CreditExchangeRecordMapper.xml
@@ -43,7 +43,7 @@
        <include refid="Base_Column_List"/> from table_credit_exchange_record where 1=1
        <include refid="listWhereSQL"/>
        <if test="query.sortList!=null">
            <foreach collection="query.sortList" item="item" open=" order by " separator=",">#{item}</foreach>
            <foreach collection="query.sortList" item="item" open=" order by " separator=",">${item}</foreach>
        </if>limit #{query.start},#{query.count}
    </select>
    <select id="count" resultType="java.lang.Long">select count(*) from table_credit_exchange_record where 1=1
src/main/resources/mapper/UserCreditRecordMapper.xml
@@ -39,7 +39,7 @@
        <include refid="Base_Column_List"/> from table_user_credit_record where 1=1
        <include refid="listWhereSQL"/>
        <if test="query.sortList!=null">
            <foreach collection="query.sortList" item="item" open=" order by " separator=",">#{item}</foreach>
            <foreach collection="query.sortList" item="item" open=" order by " separator=",">${item}</foreach>
        </if>limit #{query.start},#{query.count}
    </select>
    <select id="count" resultType="java.lang.Long">select count(*) from table_user_credit_record where 1=1
src/main/resources/static/admin/credit-exchange-record-list.html
@@ -112,6 +112,7 @@
                        }
                        return "未知";
                    }},
                    {field: 'exchangeStatusDescription', title: '状态描述', width: 180},
                    {field: 'createTime', title: '创建时间', width: 180},
                    {field: 'updateTime', title: '更新时间', width: 180}
                ]
@@ -136,7 +137,7 @@
            let ids = checkStatus.data.map(item => item.id);
            if (ids.length === 0) {
                layer.msg("请至少选择一条记录");
                return true;
                return false;
            }
            $.post('/admin/api/credit/exchange-record/approve', {ids: JSON.stringify(ids)}, function (response) {
                if (response.code === 0) {
@@ -148,6 +149,7 @@
            }, 'json').fail(function () {
                layer.msg("网络请求失败");
            });
            return false;
        });
        // 批量驳回
@@ -156,7 +158,7 @@
            let ids = checkStatus.data.map(item => item.id);
            if (ids.length === 0) {
                layer.msg("请至少选择一条记录");
                return;
                return false;
            }
            $.post('/admin/api/credit/exchange-record/reject', {ids: JSON.stringify(ids)}, function (response) {
                if (response.code === 0) {
@@ -168,6 +170,7 @@
            }, 'json').fail(function () {
                layer.msg("网络请求失败");
            });
            return false;
        });
    });
</script>
src/main/resources/static/admin/credit-setting-list.html
@@ -10,6 +10,20 @@
</head>
<body>
<form class="layui-form" action="" lay-filter='search'>
    <div class="layui-form-item">
        <div class="layui-inline">
            <input type="text" name="uid" id="uid" placeholder="用户ID" autocomplete="off" class="layui-input">
        </div>
        <div class="layui-inline">
            <button class="layui-btn layui-btn-normal" lay-submit lay-filter="search" id="search"><i
                    class="layui-icon layui-icon-search"></i>搜索
            </button>
        </div>
    </div>
</form>
    <table id="table" lay-filter="table"></table>
@@ -21,6 +35,7 @@
        layui.use(['table', 'layer'], function () {
            var table = layui.table;
            var layer = layui.layer;
            var form = layui.form;
            // 渲染表格
            table.render({
@@ -35,6 +50,13 @@
                ]],
                page: true
            });
            // 监听搜索
            form.on('submit(search)', function (data) {
                tableIns.reload();
                return false;
            });
src/main/resources/static/agent/js/index.js
@@ -71,7 +71,7 @@
            },
            getConfig: function() {
                http_util.post("/agentapi/admin/getConfig", {}, function(res) {
                http.post("/agentapi/admin/getConfig", {}, function(res) {
                    if (res.code == 0) {
                        app.config = res.data;
                    }
@@ -81,7 +81,7 @@
                });
            },
            requestOrders: function() {
                http_util.post("/agentapi/admin/orderList", {
                http.post("/agentapi/admin/orderList", {
                    key: $("#search_key").val(),
                    timeIndex: app.screen_time_index,
                    page: app.current_page
@@ -107,7 +107,7 @@
                });
            },
            requestWithdraw: function() {
                http_util.post("/agentapi/admin/withdrawList", {
                http.post("/agentapi/admin/withdrawList", {
                    page: app.current_page
                }, function(res) {
                    if (res.code == 0) {
@@ -155,7 +155,7 @@
                app.requestWithdraw();
            },
            withdraw: function(id) {
                http_util.post("/agentapi/admin/withdraw", {
                http.post("/agentapi/admin/withdraw", {
                    id: id
                }, function(res) {
                    if (res.code == 0) {
src/main/resources/static/credit/alipay_account_setting.html
@@ -12,11 +12,9 @@
            window.onresize();
        </script>
        <link rel="stylesheet" type="text/css" href="../layui/css/layui.css" />
        <link rel="stylesheet" type="text/css" href="css/common.css" />
        <script src="../js/jquery.min.js"></script>
        <script src="../js/vue.min.js"></script>
        <script src="../layui/layui.js"></script>
        <script src="js/http.js"></script>
        <style>
            body {
@@ -50,7 +48,7 @@
                height: 0.78rem;
                line-height: 0.78rem;
                background: #F6BDD3;
                border-radius: 0.39rem;
                border-radius: 0.2rem;
                margin: auto 0;
                border: none;
                color: #FFF;
@@ -59,18 +57,13 @@
            }
            
            .active {
                background-color: #FF2B4B;
                background-color: #ff5722;
            }
        </style>
    </head>
    <body>
        <div id="container" style="text-align: center;">
            <div class="top-nav">
                <div @click="goBack"></div>
                <div>支付宝设置</div>
            </div>
            <div style="height: 0.82rem;"></div>
            <div class="item">
@@ -85,17 +78,17 @@
            <button class="btn active" @click="save">保存并提交</button>
        </div>
        <script src="../js/http.js"></script>
        <script>
            $(function() {
                var app = new Vue({
                    el: "#container",
                    data: {
                        alipayName: "",
                        alipayAccount: ""
                    },
                    methods: {
                        goBack: function() {
                            if (window.history.length > 1) {
                                window.history.back();
                            }
                        },
                        save: function() {
                            var account = $("#alipayAccount").val();
                            var name = $("#alipayName").val();
@@ -108,22 +101,41 @@
                                return;
                            }
                            var index = layer.load();
                            http_util.post("/agentapi/admin/setAlipayAccount", {
                                alipayAccount: account,
                                alipayName: name
                            http.post("/credit/api/updateAlipayBinding", {
                                alipayName: name,
                                alipayAccount: account
                            }, function(res) {
                                layer.close(index);
                                if (res.code == 0) {
                                    layer.msg("设置成功");
                                    app.goBack();
                                } else {
                                    layer.msg(res.msg);
                                }
                            }, function(res) {
                            },function () {
                                layer.close(index);
                                layer.msg(res);
                                layer.msg("网络请求失败");
                            });
                        },
                        fetchAlipayBinding: function() {
                            var index = layer.load();
                            http.post("/credit/api/getAlipayBinding", {}, function(res) {
                                layer.close(index);
                                if (res.code === 0) {
                                    app.alipayName = res.data.alipayName;
                                    app.alipayAccount = res.data.alipayAccount;
                                    $("#alipayName").val(app.alipayName);
                                    $("#alipayAccount").val(app.alipayAccount);
                                } else {
                                    layer.msg(res.msg);
                                }
                            },function () {
                                layer.close(index);
                                layer.msg("网络请求失败");
                            });
                        }
                    },
                    mounted: function() {
                        this.fetchAlipayBinding();
                    }
                });
            });
src/main/resources/static/credit/credit_records.html
New file
@@ -0,0 +1,147 @@
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, viewport-fit=cover, initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
    <title>积分记录</title>
    <script>
        window.onresize = function() {
            document.documentElement.style.fontSize = document.documentElement.clientWidth / 7.5 + 'px';
        };
        window.onresize();
    </script>
    <link rel="stylesheet" type="text/css" href="../layui/css/layui.css" />
    <style>
        body {
            background-color: #E0E0E0;
        }
        .record-item {
            margin-bottom: 1px;
            padding: 0.3rem;
            background-color: #FFF;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .record-item .left {
            text-align: left;
        }
        .record-item .date {
            font-size: 0.30rem;
            color: #000000;
        }
        .record-item .description {
            font-size: 0.26rem;
            color: #666666;
            margin-top: 0.1rem;
        }
        .record-item .points {
            font-size: 0.30rem;
            font-weight: bold;
        }
        .record-item .points.increase {
            color: #FF5722;
        }
        .record-item .points.decrease {
            color: #009688;
        }
        .loading {
            text-align: center;
            padding: 0.3rem;
            color: #999;
        }
        .loading-no {
            color: #ccc;
        }
    </style>
</head>
<body>
    <div id="container">
        <div v-if="records.length > 0">
            <div class="record-item" v-for="(record, index) in records" :key="index">
                <div class="left">
                    <div class="date">{{ record.createTime }}</div>
                    <div class="description">{{ record.description }}</div>
                </div>
                <div class="points" :class="{'increase': record.direction == 1, 'decrease':record.direction == 0}">
                    <span v-if="record.direction>0">+{{record.creditAmount}}</span>
                    <span v-else>-{{ record.creditAmount }}</span>
                </div>
            </div>
            <div class="loading" @click="loadMore" :class="{'loading-no': !hasMore}">
                <span v-if="hasMore">点击加载更多</span>
                <span v-else>没有更多了</span>
            </div>
        </div>
        <div v-else class="empty">
            暂无积分记录
        </div>
    </div>
    <script src="../js/jquery.min.js"></script>
    <script src="../js/vue.min.js"></script>
    <script src="../layui/layui.js"></script>
    <script src="../js/http.js"></script>
    <script>
      let app = new Vue({
            el: '#container',
            data: {
                records: [],
                page: 1,
                hasMore: true
            },
            methods: {
                goBack: function() {
                    if (window.history.length > 1) {
                        window.history.back();
                    }
                },
                loadRecords: function() {
                    let index = layer.load();
                   http.post("/credit/api/records", {
                        page: this.page,
                        pageSize: 20
                    }, function(res) {
                        layer.close(index);
                        if (res.code == 0) {
                            var newRecords = res.data.list.map(function(item) {
                                return item;
                            });
                            app.records = app.records.concat(newRecords);
                            app.hasMore = newRecords.length === 20;
                        } else {
                            layer.msg(res.msg);
                        }
                    },function () {
                       layer.close(index);
                       layer.msg("网络请求失败");
                   });
                },
                loadMore: function() {
                    if (this.hasMore) {
                        this.page++;
                        this.loadRecords();
                    }
                }
            },
            mounted: function() {
                this.loadRecords();
            }
        });
    </script>
</body>
</html>
src/main/resources/static/credit/exchange_records.html
New file
@@ -0,0 +1,170 @@
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, viewport-fit=cover, initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
    <title>积分兑换记录</title>
    <script>
        window.onresize = function() {
            document.documentElement.style.fontSize = document.documentElement.clientWidth / 7.5 + 'px';
        };
        window.onresize();
    </script>
    <link rel="stylesheet" type="text/css" href="../layui/css/layui.css" />
    <style>
        body {
            background-color: #E0E0E0;
        }
        .record-item {
            margin-bottom: 1px;
            padding: 0.3rem;
            background-color: #FFF;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .record-item .left {
            text-align: left;
        }
        .record-item .value {
            font-size: 0.30rem;
            color: #000000;
            font-weight: bold;
        }
        .record-item .date {
            font-size: 0.26rem;
            color: #666666;
            margin-top: 0.1rem;
        }
        .record-item .points {
            font-size: 0.26rem;
            color: #999999;
            margin-top: 0.1rem;
        }
        .record-item .status {
            font-size: 0.30rem;
            font-weight: bold;
        }
        .record-item .status.pending {
            color: #FF5722;
        }
        .record-item .status.completed {
            color: #009688;
        }
        .loading {
            text-align: center;
            padding: 0.3rem;
            color: #999;
        }
        .loading-no {
            color: #ccc;
        }
    </style>
</head>
<body>
    <div id="container">
        <div v-if="records.length > 0">
            <div class="record-item" v-for="(record, index) in records" :key="index">
                <div class="left">
                    <div class="value">{{ record.exchangeValue }}元</div>
                    <div class="date">{{ record.exchangeTime }}</div>
                    <div class="points">消耗积分:{{ record.consumedCredits }}</div>
                </div>
                <div class="status" :class="record.status === 'pending' ? 'pending' : 'completed'">
                    {{ record.statusDesc }}
                </div>
            </div>
            <div class="loading" @click="loadMore" :class="{'loading-no': !hasMore}">
                <span v-if="hasMore">点击加载更多</span>
                <span v-else>没有更多了</span>
            </div>
        </div>
        <div v-else class="empty">
            暂无兑换记录
        </div>
    </div>
    <script src="../js/vue.min.js"></script>
    <script src="../layui/layui.js"></script>
    <script src="../js/http.js"></script>
    <script src="../js/jquery.min.js"></script>
    <script>
     const  app =  new Vue({
            el: '#container',
            data: {
                records: [],
                page: 1,
                hasMore: true
            },
            methods: {
                loadRecords: function() {
                    var index = layer.load();
                    http.post("/credit/api/exchange_records", {
                        page: this.page,
                        pageSize: 20
                    }, function(res) {
                        layer.close(index);
                        if (res.code === 0) {
                            var newRecords = res.data.list.map(function(item) {
                                var fitem = {
                                    exchangeTime: item.createTime,
                                    consumedCredits: item.consumedCredits,
                                    exchangeValue: item.exchangeValue,
                                    status: item.exchangeStatus===0 ? 'pending' : 'completed',
                                    statusDesc: item.exchangeStatusDescription
                                };
                                switch (item.exchangeStatus) {
                                    case 0:
                                        fitem.statusDesc = "等待审核";
                                        break;
                                    case 1:
                                        fitem.statusDesc = "兑换成功";
                                        break;
                                    case 2:
                                        fitem.statusDesc = "兑换驳回";
                                        break;
                                    default:
                                        fitem.statusDesc = "未知";
                                        break;
                                }
                                return fitem;
                            });
                            app.records = app.records.concat(newRecords);
                            app.hasMore = newRecords.length === 20;
                        } else {
                            layer.msg(res.msg);
                        }
                    },function() {
                        layer.close(index);
                        layer.msg("网络请求出错");
                    });
                },
                loadMore: function() {
                    if (this.hasMore) {
                        this.page++;
                        this.loadRecords();
                    }
                }
            },
            mounted: function() {
                this.loadRecords();
            }
        });
    </script>
</body>
</html>
src/main/resources/static/credit/index.html
@@ -3,16 +3,16 @@
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, viewport-fit=cover, initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
    <meta name="viewport"
          content="width=device-width, viewport-fit=cover, initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"/>
    <title>积分中心</title>
    <script>
        window.onresize = function() {
        window.onresize = function () {
            document.documentElement.style.fontSize = document.documentElement.clientWidth / 7.5 + 'px';
        };
        window.onresize();
    </script>
    <link rel="stylesheet" type="text/css" href="../layui/css/layui.css" />
    <link rel="stylesheet" type="text/css" href="../css/common.css" />
    <link rel="stylesheet" type="text/css" href="../layui/css/layui.css"/>
    <style>
        body {
            background-color: #FFFFFF;
@@ -35,12 +35,12 @@
        .estimated-amount {
            font-size: 0.21rem; /* 修改字体大小为原来的30% */
            color: #333;
            color: #666;
            margin-top: -0.5rem;
        }
        .exchange-container {
            text-align: center;
            text-align: left;
            margin: 1rem 0;
            padding: 1rem;
            background-color: #fff;
@@ -101,20 +101,59 @@
        .links-container a:hover {
            text-decoration: underline;
        }
        [v-cloak]{
            display: none;
        }
    </style>
    <style>
        /* 新增样式:底部菜单 */
        .footer-menu {
            position: fixed;
            bottom: 0;
            left: 0;
            width: 100%;
            display: flex;
            justify-content: space-around;
            background-color: #fff;
            border-top: 1px solid #eee;
            padding: 0.5rem 0;
            box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
        }
        .menu-item {
            display: flex;
            flex-direction: column;
            align-items: center;
            text-decoration: none;
            color: #666;
            font-size: 0.24rem;
        }
        .menu-item.active {
            color: #000;
        }
        .menu-icon {
            width: 0.4rem;
            height: 0.4rem;
            margin-bottom: 0.1rem;
        }
    </style>
</head>
<body>
<div id="app">
    <div class="exchange-container">
        <div class="balance"><span style=" color:black;">当前积分余额: </span>{{ creditBalance }}</div>
        <div class="estimated-amount">预计可兑换红包金额: {{ estimatedRedPacketAmount }}元</div>
        <div class="balance" v-cloak><span style=" color:black;">当前积分余额: </span>{{ creditBalance }}</div>
        <div class="estimated-amount" v-cloak>预计可兑换红包金额: {{ estimatedRedPacketAmount }}元</div>
        <div class="balance" v-cloak><span style=" color:black;">兑换中的积分: </span>{{ exchangingCredits }}</div>
        <div class="exchange-title">积分红包兑换</div>
        <div class="exchange-input">
            <input type="number" v-model="exchangePoints" placeholder="请输入兑换积分" />
            <input type="number" v-model="exchangePoints" placeholder="请输入兑换积分"/>
            <button class="btn" @click="exchangePointsToRedPacket">兑换</button>
        </div>
        <div class="links-container">
@@ -124,16 +163,30 @@
        </div>
    </div>
</div>
<!-- 新增底部菜单 -->
<div class="footer-menu">
    <a href="/index3.html" class="menu-item">
        <img src="../img/command.png" alt="口令" class="menu-icon">
        <span>口令</span>
    </a>
    <a href="/credit/index.html" class="menu-item active">
        <img src="../img/credit_fill.png" alt="积分" class="menu-icon">
        <span>积分</span>
    </a>
</div>
<script src="../js/jquery.min.js"></script>
<script src="../js/vue.min.js"></script>
<script src="../layui/layui.js"></script>
<script src="../js/http.js"></script>
<script src="../layui/layui.js"></script>
<script>
    new Vue({
    let app = new Vue({
        el: '#app',
        data: {
            creditBalance: 0,
            estimatedRedPacketAmount: 0,
            exchangingCredits: 0,
            exchangePoints: ''
        },
        mounted() {
@@ -141,33 +194,57 @@
        },
        methods: {
            loadCreditBalance() {
                let index = layer.load();
                // 调用API获取积分余额
                http.get('/api/credit/balance').then(response => {
                    this.creditBalance = response.data.balance;
                    this.calculateEstimatedRedPacketAmount();
                http.post("/credit/api/info", {}, function (response) {
                    layer.close(index);
                    if (response.code === 0) {
                        let data = response.data;
                        console.log("data:",data);
                        app.creditBalance = data.balance;
                        app.estimatedRedPacketAmount = data.exchangeMoney;
                    } else if (response.code === 1001) {
                        window.location.replace(response.data.link);
                    } else {
                        layer.msg(response.msg);
                    }
                }, 'json').fail(function (jqXHR, textStatus, errorThrown) {
                    layer.close(index);
                    layer.msg("网络请求失败");
                });
            },
            calculateEstimatedRedPacketAmount() {
                // 假设每100积分可兑换1元红包
                this.estimatedRedPacketAmount = (this.creditBalance / 100).toFixed(2);
            },
            exchangePointsToRedPacket() {
                if (!this.exchangePoints || this.exchangePoints <= 0) {
                    alert('请输入有效的兑换积分');
                // 校验输入的积分值是否为正整数
                const points = parseInt(this.exchangePoints, 0);
                if (isNaN(points) || points <= 0) {
                    layer.msg('请输入有效的兑换积分');
                    return;
                }
                // 校验用户积分余额是否足够
                if (points > this.creditBalance) {
                    layer.msg('积分余额不足');
                    return;
                }
                let index = layer.load();
                // 调用API进行积分兑换
                http.post('/api/credit/exchange', { points: this.exchangePoints }).then(response => {
                    alert('兑换成功');
                    this.loadCreditBalance();
                    this.exchangePoints = '';
                }).catch(error => {
                    alert('兑换失败: ' + error.message);
                http.post('/credit/api/exchange', {"credits":points}, function(response){
                    layer.close(index);
                    if(response.code===0){
                        layer.msg("提交成功,等待审核");
                        app.loadCreditBalance();
                    }else{
                        layer.msg(response.msg);
                    }
                },function (){
                    layer.close(index);
                    layer.msg("网络请求失败");
                });
            }
        }
    });
</script>
</body>
</html>
src/main/resources/static/img/command.png
src/main/resources/static/img/command_fill.png
src/main/resources/static/img/credit.png
src/main/resources/static/img/credit_fill.png
src/main/resources/static/index3.html
@@ -5,6 +5,12 @@
        <meta name="viewport" content="width=device-width, viewport-fit=cover, initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
        <title>福利发放</title>
        <link rel="stylesheet" type="text/css" href="layui/css/layui.css" />
        <script>
            window.onresize = function () {
                document.documentElement.style.fontSize = document.documentElement.clientWidth / 7.5 + 'px';
            };
            window.onresize();
        </script>
        <script src="js/jquery.min.js"></script>
        <script src="layui/layui.js"></script>
        <style>
@@ -17,23 +23,23 @@
                 flex-direction: column;
            }
            .edit{
                border-radius: 1rem;
                border-radius: 0.2rem;
                width: 80%;
                margin-top: 3rem;
                margin-top: 0.6rem;
            }
            .money {
                border-radius: 0.5rem;
                border-radius: 0.1rem;
                width: 80%;
                margin-top: 2rem;
                margin-top: 0.4rem;
                display: none;
            }
            
            .btn{
                background-color: #FF2B4B;
                border-radius: 0.5rem;
                border-radius: 0.1rem;
                width: 80%;
                margin-top: 2rem;
                font-size: 1.2rem;
                margin-top: 0.4rem;
                font-size: 0.25rem;
            }
            
        </style>
@@ -50,6 +56,53 @@
            <button class="layui-btn btn" >提交</button>
            
        </div>
        <!-- 新增底部菜单 -->
        <div class="footer-menu">
            <a href="/index3.html" class="menu-item">
                <img src="../img/command_fill.png" alt="口令" class="menu-icon">
                <span>口令</span>
            </a>
            <a href="/credit/index.html" class="menu-item active">
                <img src="../img/credit.png" alt="积分" class="menu-icon">
                <span>积分</span>
            </a>
        </div>
        <style>
            /* 新增样式:底部菜单 */
            .footer-menu {
                position: fixed;
                bottom: 0;
                left: 0;
                width: 100%;
                display: flex;
                justify-content: space-around;
                background-color: #fff;
                border-top: 1px solid #eee;
                padding: 0.5rem 0;
                box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
            }
            .menu-item {
                display: flex;
                flex-direction: column;
                align-items: center;
                text-decoration: none;
                color: #666;
                font-size: 0.24rem;
            }
            .menu-item.active {
                color: #000;
            }
            .menu-icon {
                width: 0.4rem;
                height: 0.4rem;
                margin-bottom: 0.1rem;
            }
        </style>
        
        <script>
            $(function(){
src/main/resources/static/js/http.js
New file
@@ -0,0 +1,21 @@
const http = {
    getQueryString: function (name) {
    },
    post: function (url, params, success_callback, fail_callback) {
        $.post(url, params, function (res) {
            success_callback(res)
            if (res.code === 1001) {
                window.location.replace(res.data.link);
            }
        }, 'json').fail(function (jqXHR, textStatus, errorThrown) {
            layer.msg("网络请求失败");
            if (fail_callback) {
                fail_callback();
            }
        });
    },
};