17 changed files with 684 additions and 1 deletions
@ -0,0 +1,18 @@ |
|||
package awesome.group.game.dao.bean; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.IdType; |
|||
import com.baomidou.mybatisplus.annotation.TableId; |
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class MatrixUser { |
|||
@TableId(value = "id", type = IdType.AUTO) |
|||
private Integer id; |
|||
private String mobile; |
|||
private Integer appId; |
|||
private String name; |
|||
private String aliPayAccount; |
|||
private String pwd; |
|||
private String inviteCode; |
|||
private Integer upUid; |
|||
} |
@ -0,0 +1,14 @@ |
|||
package awesome.group.game.dao.mapper; |
|||
|
|||
import awesome.group.game.dao.bean.MatrixUser; |
|||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
|||
import org.apache.ibatis.annotations.Select; |
|||
|
|||
public interface MatrixUserMapper extends BaseMapper<MatrixUser> { |
|||
|
|||
@Select("select * from matrix_user where invite_code = #{inviteCode}") |
|||
MatrixUser selectByInviteCode(String inviteCode); |
|||
|
|||
@Select("select * from matrix_user where app_id = #{appId} and mobile = #{mobile}") |
|||
MatrixUser selectByAppIdAndMobile(int appId, String mobile); |
|||
} |
@ -0,0 +1,115 @@ |
|||
package awesome.group.game.service; |
|||
|
|||
import awesome.group.game.service.bo.STSInfo; |
|||
import awesome.group.game.service.cache.CacheKey; |
|||
import awesome.group.game.service.cache.JedisManager; |
|||
import awesome.group.game.service.common.exception.PaganiException; |
|||
import awesome.group.game.service.common.exception.PaganiExceptionCode; |
|||
import awesome.group.game.service.common.log.L; |
|||
import com.aliyun.oss.OSS; |
|||
import com.aliyun.oss.OSSClientBuilder; |
|||
import com.aliyun.oss.OSSException; |
|||
import com.aliyuncs.DefaultAcsClient; |
|||
import com.aliyuncs.IAcsClient; |
|||
import com.aliyuncs.exceptions.ClientException; |
|||
import com.aliyuncs.profile.DefaultProfile; |
|||
import com.aliyuncs.sts.model.v20150401.AssumeRoleRequest; |
|||
import com.aliyuncs.sts.model.v20150401.AssumeRoleResponse; |
|||
|
|||
import com.google.gson.Gson; |
|||
import jakarta.annotation.Resource; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.io.IOException; |
|||
import java.io.InputStream; |
|||
import java.net.URL; |
|||
import java.net.URLConnection; |
|||
|
|||
/** |
|||
* @author nidaren |
|||
*/ |
|||
@Service |
|||
public class OSSService { |
|||
private static final String accessKeyId = "LTAI5tHker5udo3UNAjxjykg"; |
|||
private static final String accessKeySecret = "WT7BLX9w1muzYYmI0ZdMsyI15Iq7v5"; |
|||
private static final String roleArn = "acs:ram::1464656672772974:role/oss-upload"; |
|||
private static final String endpoint = "oss-cn-beijing.aliyuncs.com"; |
|||
private static final String bucket = "bzgames"; |
|||
public static final String resourcePrefix = "https://bzgames.oss-cn-beijing.aliyuncs.com"; |
|||
|
|||
private IAcsClient client = null; |
|||
|
|||
@Resource |
|||
private JedisManager jedisManager; |
|||
|
|||
|
|||
public synchronized IAcsClient getOrCreatClient() { |
|||
if (client != null) { |
|||
return client; |
|||
} |
|||
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret); |
|||
client = new DefaultAcsClient(profile); |
|||
return this.client; |
|||
} |
|||
|
|||
public STSInfo getSTS(Integer uid) { |
|||
String key = CacheKey.stsKey(uid); |
|||
String res = jedisManager.get(key); |
|||
Gson gson = new Gson(); |
|||
if (StringUtils.isNotBlank(res)) { |
|||
return gson.fromJson(res, STSInfo.class); |
|||
} |
|||
|
|||
String roleSessionName = "uid_" + uid; |
|||
AssumeRoleRequest request = new AssumeRoleRequest(); |
|||
request.setRoleArn(roleArn); |
|||
request.setRoleSessionName(roleSessionName); |
|||
request.setDurationSeconds(900L); |
|||
try { |
|||
AssumeRoleResponse response = getOrCreatClient().getAcsResponse(request); |
|||
STSInfo info = new STSInfo(response.getCredentials()); |
|||
info.stsRole = roleArn; |
|||
info.bucket = bucket; |
|||
info.endpoint = endpoint; |
|||
jedisManager.set(key, gson.toJson(info), 900); |
|||
return info; |
|||
} catch (ClientException e) { |
|||
L.trace("aliyun_sts_fail", "", e); |
|||
} |
|||
throw new PaganiException(PaganiExceptionCode.GENERAL_ERROR, "请稍后再试"); |
|||
} |
|||
|
|||
public static String getFullResourceUrl(String path) { |
|||
if (path.startsWith("http")) { |
|||
return path; |
|||
} |
|||
if (path.startsWith("/")) { |
|||
return resourcePrefix + path; |
|||
} |
|||
return resourcePrefix + "/" + path; |
|||
} |
|||
|
|||
public String simpleUpload(String url, Integer uid) { |
|||
STSInfo info = getSTS(uid); |
|||
OSS ossClient = new OSSClientBuilder().build("https://oss-cn-hangzhou.aliyuncs.com", info.accessKeyId, info.accessKeySecret, info.securityToken); |
|||
try { |
|||
URLConnection connection = new URL(url).openConnection(); |
|||
String mimeType = connection.getContentType(); |
|||
InputStream inputStream = connection.getInputStream(); |
|||
|
|||
String fileType = mimeType.split("/")[1]; |
|||
String fileName = "mario/" + System.currentTimeMillis() + "." + fileType; |
|||
String msg = String.format("url:%s, uid: %s, fileName: %s", url, uid, fileName); |
|||
ossClient.putObject(bucket, fileName, inputStream); |
|||
L.trace("upload_success", msg); |
|||
return String.format("https://%s.%s/%s", bucket, endpoint, fileName); |
|||
} catch (OSSException e) { |
|||
L.trace("upload_success", "url:" + url + ",uid:" + uid); |
|||
throw new PaganiException(PaganiExceptionCode.GENERAL_ERROR, "上传失败"); |
|||
} catch (IOException e) { |
|||
L.trace("download_fail", "url:" + url + ",uid:" + uid); |
|||
throw new PaganiException(PaganiExceptionCode.GENERAL_ERROR, "非法url"); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,69 @@ |
|||
package awesome.group.game.service; |
|||
|
|||
import awesome.group.game.dao.bean.MatrixApp; |
|||
import awesome.group.game.dao.bean.MatrixUser; |
|||
import awesome.group.game.dao.mapper.MatrixAppMapper; |
|||
import awesome.group.game.dao.mapper.MatrixUserMapper; |
|||
import awesome.group.game.service.bo.MatrixAppBo; |
|||
import awesome.group.game.service.bo.RegisterBo; |
|||
import awesome.group.game.service.common.exception.PaganiException; |
|||
import awesome.group.game.service.common.exception.PaganiExceptionCode; |
|||
import awesome.group.game.service.util.EncryptUtil; |
|||
import org.apache.commons.lang.RandomStringUtils; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.util.Assert; |
|||
import org.springframework.util.StringUtils; |
|||
|
|||
import java.util.concurrent.ThreadLocalRandom; |
|||
|
|||
@Service |
|||
public class RegisterService { |
|||
@Autowired |
|||
private MatrixAppMapper appMapper; |
|||
|
|||
@Autowired |
|||
private MatrixUserMapper userMapper; |
|||
|
|||
@Autowired |
|||
private SmsService smsService; |
|||
|
|||
public MatrixAppBo getApp(String inviteCode) { |
|||
MatrixUser user = userMapper.selectByInviteCode(inviteCode); |
|||
if (user == null) { |
|||
throw new PaganiException(PaganiExceptionCode.GENERAL_ERROR, "邀请码非法"); |
|||
} |
|||
int appId = user.getAppId(); |
|||
MatrixApp app = appMapper.selectById(appId); |
|||
return new MatrixAppBo(app); |
|||
} |
|||
|
|||
public void register(RegisterBo bo) { |
|||
Assert.isTrue(StringUtils.hasText(bo.mobile), "手机号不能为空"); |
|||
Assert.isTrue(StringUtils.hasText(bo.inviteCode), "邀请码不能为空"); |
|||
Assert.isTrue(StringUtils.hasText(bo.code), "验证码不能为空"); |
|||
Assert.isTrue(StringUtils.hasText(bo.pwd), "密码不能为空"); |
|||
|
|||
if (smsService.verifyAndUseCaptcha(bo.mobile, "register", bo.code)) { |
|||
throw new PaganiException(PaganiExceptionCode.GENERAL_ERROR, "验证码错误"); |
|||
} |
|||
|
|||
MatrixUser prev = userMapper.selectByInviteCode(bo.inviteCode); |
|||
if (prev == null) { |
|||
throw new PaganiException(PaganiExceptionCode.GENERAL_ERROR, "邀请码非法"); |
|||
} |
|||
int appId = prev.getAppId(); |
|||
MatrixUser u = userMapper.selectByAppIdAndMobile(appId, bo.mobile); |
|||
if (u != null) { |
|||
throw new PaganiException(PaganiExceptionCode.GENERAL_ERROR, "手机号已注册"); |
|||
} |
|||
|
|||
u = new MatrixUser(); |
|||
u.setMobile(bo.mobile); |
|||
u.setAppId(appId); |
|||
u.setPwd(EncryptUtil.sha1(bo.pwd)); |
|||
u.setInviteCode(RandomStringUtils.randomAlphabetic(12)); |
|||
u.setUpUid(prev.getId()); |
|||
userMapper.insert(u); |
|||
} |
|||
} |
@ -0,0 +1,108 @@ |
|||
package awesome.group.game.service; |
|||
|
|||
import awesome.group.game.service.cache.CacheKey; |
|||
import awesome.group.game.service.cache.JedisManager; |
|||
import awesome.group.game.service.common.exception.PaganiException; |
|||
import awesome.group.game.service.common.exception.PaganiExceptionCode; |
|||
import awesome.group.game.service.common.log.L; |
|||
import com.aliyun.dysmsapi20170525.Client; |
|||
import com.aliyun.dysmsapi20170525.models.SendSmsRequest; |
|||
import com.aliyun.dysmsapi20170525.models.SendSmsResponse; |
|||
import com.aliyun.teaopenapi.models.Config; |
|||
import com.google.gson.Gson; |
|||
import jakarta.annotation.Resource; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.util.Arrays; |
|||
import java.util.List; |
|||
import java.util.Random; |
|||
|
|||
/** |
|||
* @author nidaren |
|||
*/ |
|||
@Service |
|||
public class SmsService { |
|||
|
|||
|
|||
public static final String accessKeyId = "LTAI5tD27oA2VTasbZcjCQN7"; |
|||
public static final String accessSecret = "1kGGBrLy4yIjAEUXCGKaOHTfjJegK8"; |
|||
public static final String signName = "瑞双科技"; |
|||
public static final String templateCode = "SMS_217850700"; |
|||
|
|||
public static final int expireTime = 1800; |
|||
|
|||
private static Client client; |
|||
|
|||
@Resource |
|||
private JedisManager jedisManager; |
|||
|
|||
public SmsService() { |
|||
Config config = new Config() |
|||
// 您的AccessKey ID
|
|||
.setAccessKeyId(accessKeyId) |
|||
// 您的AccessKey Secret
|
|||
.setAccessKeySecret(accessSecret); |
|||
config.endpoint = "dysmsapi.aliyuncs.com"; |
|||
try { |
|||
client = new Client(config); |
|||
} catch (Exception e) { |
|||
L.trace("init_sms_client_error", "", e); |
|||
} |
|||
} |
|||
|
|||
public void sendCaptcha(String phone, String scene) { |
|||
List<String> sceneList = Arrays.asList("register"); |
|||
if (!sceneList.contains(scene)) { |
|||
throw new PaganiException(PaganiExceptionCode.GENERAL_ERROR, "非法scene"); |
|||
} |
|||
String key = CacheKey.captchaKey(scene, phone); |
|||
if (!StringUtils.isEmpty(jedisManager.get(key))) { |
|||
return;//验证码有效期内只发一次
|
|||
} |
|||
SendSmsRequest request = new SendSmsRequest(); |
|||
request.phoneNumbers = phone; |
|||
request.signName = signName; |
|||
request.templateCode = templateCode; |
|||
String code = getRandom(); |
|||
request.templateParam = String.format("{\"code\": \"%s\"}", code); |
|||
Gson gson = new Gson(); |
|||
|
|||
try { |
|||
SendSmsResponse response = client.sendSms(request); |
|||
String msg = String.format("request: %s, response: %s", gson.toJson(request), gson.toJson(response.body)); |
|||
if (response.body.code.equals("OK")) { |
|||
L.trace("send_sms_success", msg); |
|||
} else { |
|||
L.trace("send_sms_fail", msg); |
|||
throw new PaganiException(PaganiExceptionCode.GENERAL_ERROR, "短信发送失败"); |
|||
} |
|||
jedisManager.set(key, code, expireTime); |
|||
} catch (Exception e) { |
|||
L.trace("send_sms_fail", gson.toJson(request), e); |
|||
throw new PaganiException(PaganiExceptionCode.GENERAL_ERROR, "短信发送失败"); |
|||
} |
|||
} |
|||
|
|||
public boolean verifyAndUseCaptcha(String phone, String scene, String code) { |
|||
String key = CacheKey.captchaKey(scene, phone); |
|||
String redisCode = jedisManager.get(key); |
|||
if (StringUtils.isEmpty(redisCode)) { |
|||
return false; |
|||
} |
|||
if (!redisCode.equals(code)) { |
|||
return false; |
|||
} |
|||
jedisManager.del(key); |
|||
return true; |
|||
} |
|||
|
|||
public static String getRandom() { |
|||
StringBuilder builder = new StringBuilder(); |
|||
Random ran = new Random(); |
|||
for (int i = 0; i < 6; i++) { |
|||
builder.append(ran.nextInt(10)); |
|||
} |
|||
return builder.toString(); |
|||
} |
|||
} |
@ -0,0 +1,20 @@ |
|||
package awesome.group.game.service.bo; |
|||
|
|||
import awesome.group.game.dao.bean.MatrixApp; |
|||
|
|||
public class MatrixAppBo { |
|||
public String name; |
|||
public String code; |
|||
public String img; |
|||
public String url; |
|||
|
|||
public MatrixAppBo() { |
|||
} |
|||
|
|||
public MatrixAppBo(MatrixApp app) { |
|||
this.name = app.getName(); |
|||
this.code = app.getCode(); |
|||
this.img = app.getImg(); |
|||
this.url = app.getUrl(); |
|||
} |
|||
} |
@ -0,0 +1,8 @@ |
|||
package awesome.group.game.service.bo; |
|||
|
|||
public class RegisterBo { |
|||
public String mobile; |
|||
public String code;//验证码
|
|||
public String pwd; |
|||
public String inviteCode; |
|||
} |
@ -0,0 +1,26 @@ |
|||
package awesome.group.game.service.bo; |
|||
|
|||
import com.aliyuncs.sts.model.v20150401.AssumeRoleResponse; |
|||
|
|||
/** |
|||
* @author nidaren |
|||
*/ |
|||
public class STSInfo { |
|||
public String securityToken; |
|||
public String expiration; |
|||
public String accessKeySecret; |
|||
public String accessKeyId; |
|||
public String stsRole; |
|||
public String bucket; |
|||
public String endpoint; |
|||
|
|||
public STSInfo() { |
|||
} |
|||
|
|||
public STSInfo(AssumeRoleResponse.Credentials credentials) { |
|||
this.securityToken = credentials.getSecurityToken(); |
|||
this.accessKeyId = credentials.getAccessKeyId(); |
|||
this.accessKeySecret = credentials.getAccessKeySecret(); |
|||
this.expiration = credentials.getExpiration(); |
|||
} |
|||
} |
@ -0,0 +1,18 @@ |
|||
package awesome.group.game.service.cache; |
|||
|
|||
/** |
|||
* @author nidaren |
|||
*/ |
|||
public class CacheKey { |
|||
public static final String captchaKey(String scene, String phone) { |
|||
return String.format("captcha_%s_%s", scene, phone); |
|||
} |
|||
|
|||
public static final String stsKey(Integer uid) { |
|||
return String.format("aliyun_sts_%s", uid); |
|||
} |
|||
|
|||
public static final String activateTryTimes(Integer uid) { |
|||
return String.format("vip_activate_times_%s", uid); |
|||
} |
|||
} |
@ -0,0 +1,166 @@ |
|||
package awesome.group.game.service.cache; |
|||
|
|||
import jakarta.annotation.PostConstruct; |
|||
import jakarta.annotation.PreDestroy; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.util.Assert; |
|||
import org.springframework.util.StringUtils; |
|||
import redis.clients.jedis.Jedis; |
|||
import redis.clients.jedis.JedisPool; |
|||
import redis.clients.jedis.JedisPoolConfig; |
|||
import redis.clients.jedis.Pipeline; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.Set; |
|||
|
|||
@Component |
|||
public class JedisManager { |
|||
|
|||
@Value("${spring.redis.maxTotal}") |
|||
private int maxTotal; |
|||
|
|||
@Value("${spring.redis.poolMaxWaitMs}") |
|||
private int poolMaxWaitMs; |
|||
|
|||
@Value("${spring.redis.timeOutMs}") |
|||
private int timeOutMs; |
|||
|
|||
@Value("${spring.redis.host}") |
|||
private String host; |
|||
|
|||
@Value("${spring.redis.port}") |
|||
private int port; |
|||
|
|||
@Value("${spring.redis.password}") |
|||
private String password; |
|||
|
|||
private JedisPool jedisPool = null; |
|||
|
|||
|
|||
@PostConstruct |
|||
public void init() { |
|||
JedisPoolConfig config = new JedisPoolConfig(); |
|||
config.setMaxTotal(maxTotal); |
|||
config.setMaxWaitMillis(poolMaxWaitMs); |
|||
config.setMaxIdle(maxTotal); |
|||
config.setMinIdle(4); |
|||
config.setBlockWhenExhausted(true); |
|||
config.setTestWhileIdle(true); |
|||
if (StringUtils.isEmpty(password)) { |
|||
password = null; |
|||
} |
|||
jedisPool = new JedisPool(config, host, port, timeOutMs, password); |
|||
} |
|||
|
|||
@PreDestroy |
|||
public void destroy() { |
|||
jedisPool.close(); |
|||
} |
|||
|
|||
public Jedis getJedis() { |
|||
return jedisPool.getResource(); |
|||
} |
|||
|
|||
|
|||
public void batchWriteCache(ArrayList<String> keys, ArrayList<String> values, int expire) { |
|||
Assert.notEmpty(keys, "keys is empty"); |
|||
Assert.notEmpty(values, "values is empty"); |
|||
Assert.isTrue(expire > 0, "expire is required"); |
|||
Assert.isTrue(keys.size() == values.size(), "key size not eq values size"); |
|||
List<String> kvs = new ArrayList<>(keys.size() * 2); |
|||
for (int i = 0; i < keys.size(); i++) { |
|||
kvs.add(keys.get(i)); |
|||
kvs.add(values.get(i)); |
|||
} |
|||
try (Jedis jedis = getJedis()) { |
|||
Pipeline pipeline = jedis.pipelined(); |
|||
pipeline.mset(kvs.toArray(new String[kvs.size()])); |
|||
keys.forEach(k -> pipeline.expire(k, expire)); |
|||
pipeline.sync(); |
|||
} catch (Exception e) { |
|||
throw e; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 不存在的key,数组中对应位置值为null |
|||
* |
|||
* @param keys |
|||
* @return |
|||
*/ |
|||
public List<String> batchGet(List<String> keys) { |
|||
Assert.notEmpty(keys, "keys is empty"); |
|||
try (Jedis jedis = getJedis()) { |
|||
return jedis.mget(keys.toArray(new String[keys.size()])); |
|||
} catch (Exception e) { |
|||
throw e; |
|||
} |
|||
} |
|||
|
|||
public String get(String key) { |
|||
Assert.notNull(key, "key is null"); |
|||
|
|||
try (Jedis jedis = getJedis()) { |
|||
return jedis.get(key); |
|||
} catch (Exception e) { |
|||
throw e; |
|||
} |
|||
} |
|||
|
|||
public void set(String key, String value, int expireSeconds) { |
|||
Assert.notNull(key, "key is null"); |
|||
Assert.notNull(value, "value is null"); |
|||
|
|||
try (Jedis jedis = getJedis()) { |
|||
jedis.setex(key, expireSeconds, value); |
|||
} catch (Exception e) { |
|||
throw e; |
|||
} |
|||
} |
|||
|
|||
public void del(String key) { |
|||
try (Jedis jedis = getJedis()) { |
|||
jedis.del(key); |
|||
} catch (Exception e) { |
|||
throw e; |
|||
} |
|||
} |
|||
|
|||
public long setnx(String key, String v) { |
|||
try (Jedis jedis = getJedis()) { |
|||
return jedis.setnx(key, v); |
|||
} catch (Exception e) { |
|||
throw e; |
|||
} |
|||
} |
|||
|
|||
public Set<String> sMember(String key) { |
|||
Assert.notNull(key, "key is null"); |
|||
try (Jedis jedis = getJedis()) { |
|||
return jedis.smembers(key); |
|||
} catch (Exception e) { |
|||
throw e; |
|||
} |
|||
} |
|||
|
|||
public long incr(String key) { |
|||
Assert.notNull(key, "key is null"); |
|||
try (Jedis jedis = getJedis()) { |
|||
return jedis.incr(key); |
|||
} catch (Exception e) { |
|||
throw e; |
|||
} |
|||
} |
|||
|
|||
public void setExpire(String key, Integer seconds) { |
|||
Assert.notNull(key, "key is null"); |
|||
try (Jedis jedis = getJedis()) { |
|||
jedis.expire(key, seconds); |
|||
} catch (Exception e) { |
|||
throw e; |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,42 @@ |
|||
package awesome.group.game.web.rest.citrus; |
|||
|
|||
import awesome.group.game.service.RegisterService; |
|||
import awesome.group.game.service.SmsService; |
|||
import awesome.group.game.service.bo.MatrixAppBo; |
|||
import awesome.group.game.service.bo.RegisterBo; |
|||
import awesome.group.game.service.common.response.R; |
|||
import awesome.group.game.web.aop.RestApi; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
@RestController |
|||
@RequestMapping("/api/citrus/register") |
|||
public class RegisterController { |
|||
|
|||
@Autowired |
|||
private RegisterService registerService; |
|||
|
|||
@Autowired |
|||
private SmsService smsService; |
|||
|
|||
@PostMapping("/sendCode") |
|||
@RestApi |
|||
public R<Void> sendCode(@RequestParam String mobile) { |
|||
smsService.sendCaptcha(mobile, "register"); |
|||
return new R<>(null); |
|||
} |
|||
|
|||
@GetMapping("/getApp") |
|||
@RestApi |
|||
public R<MatrixAppBo> getApp(@RequestParam String inviteCode) { |
|||
return new R<>(registerService.getApp(inviteCode)); |
|||
} |
|||
|
|||
@PostMapping("/submitRegister") |
|||
@RestApi |
|||
public R<Void> submitRegister(@RequestBody RegisterBo bo) { |
|||
registerService.register(bo); |
|||
return new R<>(null); |
|||
} |
|||
|
|||
} |
@ -0,0 +1 @@ |
|||
citrus名字是乱起的,对应的前端项目是pomelo,这个名字是我随便起的,因为我喜欢吃柑橘和柚子,所以就叫citrus和pomelo了,没有任何含义,只是为了区分不同的项目而已。 |
Loading…
Reference in new issue