Merge branch 'master' into a123lsw-patch-2
This commit is contained in:
@@ -509,6 +509,23 @@ public class AccountCurrentController(
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("devices/{deviceId}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<AuthSession>> DeleteDevice(string deviceId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
await accounts.DeleteDevice(currentUser, deviceId);
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("sessions/current")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<AuthSession>> DeleteCurrentSession()
|
||||
@@ -527,14 +544,15 @@ public class AccountCurrentController(
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPatch("devices/{id}/label")]
|
||||
public async Task<ActionResult<AuthSession>> UpdateDeviceLabel(string id, [FromBody] string label)
|
||||
[HttpPatch("devices/{deviceId}/label")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<AuthSession>> UpdateDeviceLabel(string deviceId, [FromBody] string label)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
await accounts.UpdateDeviceName(currentUser, id, label);
|
||||
await accounts.UpdateDeviceName(currentUser, deviceId, label);
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -544,6 +562,7 @@ public class AccountCurrentController(
|
||||
}
|
||||
|
||||
[HttpPatch("devices/current/label")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<AuthSession>> UpdateCurrentDeviceLabel([FromBody] string label)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
|
||||
@@ -738,4 +757,4 @@ public class AccountCurrentController(
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -464,7 +464,9 @@ public class AccountService(
|
||||
|
||||
public async Task<AuthClient> UpdateDeviceName(Account account, string deviceId, string label)
|
||||
{
|
||||
var device = await db.AuthClients.FirstOrDefaultAsync(d => d.DeviceId == deviceId && d.AccountId == account.Id);
|
||||
var device = await db.AuthClients.FirstOrDefaultAsync(
|
||||
c => c.DeviceId == deviceId && c.AccountId == account.Id
|
||||
);
|
||||
if (device is null) throw new InvalidOperationException("Device was not found.");
|
||||
|
||||
device.DeviceLabel = label;
|
||||
@@ -492,7 +494,7 @@ public class AccountService(
|
||||
{
|
||||
if (!await IsDeviceActive(session.Challenge.ClientId.Value))
|
||||
await pusher.UnsubscribePushNotificationsAsync(new UnsubscribePushNotificationsRequest()
|
||||
{ DeviceId = session.Challenge.Client!.DeviceId }
|
||||
{ DeviceId = session.Challenge.Client!.DeviceId }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -506,6 +508,36 @@ public class AccountService(
|
||||
await cache.RemoveAsync($"{DysonTokenAuthHandler.AuthCachePrefix}{item.Id}");
|
||||
}
|
||||
|
||||
public async Task DeleteDevice(Account account, string deviceId)
|
||||
{
|
||||
var device = await db.AuthClients.FirstOrDefaultAsync(
|
||||
c => c.DeviceId == deviceId && c.AccountId == account.Id
|
||||
);
|
||||
if (device is null)
|
||||
throw new InvalidOperationException("Device not found.");
|
||||
|
||||
await pusher.UnsubscribePushNotificationsAsync(
|
||||
new UnsubscribePushNotificationsRequest() { DeviceId = device.DeviceId }
|
||||
);
|
||||
|
||||
db.AuthClients.Remove(device);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var sessions = await db.AuthSessions
|
||||
.Include(s => s.Challenge)
|
||||
.Where(s => s.Challenge.ClientId == device.Id)
|
||||
.ToListAsync();
|
||||
|
||||
// The current session should be included in the sessions' list
|
||||
await db.AuthSessions
|
||||
.Include(s => s.Challenge)
|
||||
.Where(s => s.Challenge.DeviceId == device.DeviceId)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
foreach (var item in sessions)
|
||||
await cache.RemoveAsync($"{DysonTokenAuthHandler.AuthCachePrefix}{item.Id}");
|
||||
}
|
||||
|
||||
public async Task<AccountContact> CreateContactMethod(Account account, AccountContactType type, string content)
|
||||
{
|
||||
var isExists = await db.AccountContacts
|
||||
|
@@ -18,35 +18,35 @@ public class AppDatabase(
|
||||
IConfiguration configuration
|
||||
) : DbContext(options)
|
||||
{
|
||||
public DbSet<PermissionNode> PermissionNodes { get; set; }
|
||||
public DbSet<PermissionGroup> PermissionGroups { get; set; }
|
||||
public DbSet<PermissionGroupMember> PermissionGroupMembers { get; set; }
|
||||
public DbSet<PermissionNode> PermissionNodes { get; set; } = null!;
|
||||
public DbSet<PermissionGroup> PermissionGroups { get; set; } = null!;
|
||||
public DbSet<PermissionGroupMember> PermissionGroupMembers { get; set; } = null!;
|
||||
|
||||
public DbSet<MagicSpell> MagicSpells { get; set; }
|
||||
public DbSet<Account.Account> Accounts { get; set; }
|
||||
public DbSet<AccountConnection> AccountConnections { get; set; }
|
||||
public DbSet<AccountProfile> AccountProfiles { get; set; }
|
||||
public DbSet<AccountContact> AccountContacts { get; set; }
|
||||
public DbSet<AccountAuthFactor> AccountAuthFactors { get; set; }
|
||||
public DbSet<Relationship> AccountRelationships { get; set; }
|
||||
public DbSet<Status> AccountStatuses { get; set; }
|
||||
public DbSet<CheckInResult> AccountCheckInResults { get; set; }
|
||||
public DbSet<AccountBadge> Badges { get; set; }
|
||||
public DbSet<ActionLog> ActionLogs { get; set; }
|
||||
public DbSet<AbuseReport> AbuseReports { get; set; }
|
||||
public DbSet<MagicSpell> MagicSpells { get; set; } = null!;
|
||||
public DbSet<Account.Account> Accounts { get; set; } = null!;
|
||||
public DbSet<AccountConnection> AccountConnections { get; set; } = null!;
|
||||
public DbSet<AccountProfile> AccountProfiles { get; set; } = null!;
|
||||
public DbSet<AccountContact> AccountContacts { get; set; } = null!;
|
||||
public DbSet<AccountAuthFactor> AccountAuthFactors { get; set; } = null!;
|
||||
public DbSet<Relationship> AccountRelationships { get; set; } = null!;
|
||||
public DbSet<Status> AccountStatuses { get; set; } = null!;
|
||||
public DbSet<CheckInResult> AccountCheckInResults { get; set; } = null!;
|
||||
public DbSet<AccountBadge> Badges { get; set; } = null!;
|
||||
public DbSet<ActionLog> ActionLogs { get; set; } = null!;
|
||||
public DbSet<AbuseReport> AbuseReports { get; set; } = null!;
|
||||
|
||||
public DbSet<AuthSession> AuthSessions { get; set; }
|
||||
public DbSet<AuthChallenge> AuthChallenges { get; set; }
|
||||
public DbSet<AuthClient> AuthClients { get; set; }
|
||||
public DbSet<AuthSession> AuthSessions { get; set; } = null!;
|
||||
public DbSet<AuthChallenge> AuthChallenges { get; set; } = null!;
|
||||
public DbSet<AuthClient> AuthClients { get; set; } = null!;
|
||||
|
||||
public DbSet<Wallet.Wallet> Wallets { get; set; }
|
||||
public DbSet<WalletPocket> WalletPockets { get; set; }
|
||||
public DbSet<Order> PaymentOrders { get; set; }
|
||||
public DbSet<Transaction> PaymentTransactions { get; set; }
|
||||
public DbSet<Subscription> WalletSubscriptions { get; set; }
|
||||
public DbSet<Coupon> WalletCoupons { get; set; }
|
||||
public DbSet<Wallet.Wallet> Wallets { get; set; } = null!;
|
||||
public DbSet<WalletPocket> WalletPockets { get; set; } = null!;
|
||||
public DbSet<Order> PaymentOrders { get; set; } = null!;
|
||||
public DbSet<Transaction> PaymentTransactions { get; set; } = null!;
|
||||
public DbSet<Subscription> WalletSubscriptions { get; set; } = null!;
|
||||
public DbSet<Coupon> WalletCoupons { get; set; } = null!;
|
||||
|
||||
public DbSet<Punishment> Punishments { get; set; }
|
||||
public DbSet<Punishment> Punishments { get; set; } = null!;
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
|
@@ -157,14 +157,14 @@ public class DysonTokenAuthHandler(
|
||||
{
|
||||
// Handle JWT tokens (3 parts)
|
||||
case 3:
|
||||
{
|
||||
var (isValid, jwtResult) = oidc.ValidateToken(token);
|
||||
if (!isValid) return false;
|
||||
var jti = jwtResult?.Claims.FirstOrDefault(c => c.Type == "jti")?.Value;
|
||||
if (jti is null) return false;
|
||||
{
|
||||
var (isValid, jwtResult) = oidc.ValidateToken(token);
|
||||
if (!isValid) return false;
|
||||
var jti = jwtResult?.Claims.FirstOrDefault(c => c.Type == "jti")?.Value;
|
||||
if (jti is null) return false;
|
||||
|
||||
return Guid.TryParse(jti, out sessionId);
|
||||
}
|
||||
return Guid.TryParse(jti, out sessionId);
|
||||
}
|
||||
// Handle compact tokens (2 parts)
|
||||
case 2:
|
||||
// Original compact token validation logic
|
||||
@@ -190,8 +190,6 @@ public class DysonTokenAuthHandler(
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -277,4 +275,4 @@ public class DysonTokenAuthHandler(
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -20,19 +20,19 @@ public class AuthController(
|
||||
) : ControllerBase
|
||||
{
|
||||
private readonly string _cookieDomain = configuration["AuthToken:CookieDomain"]!;
|
||||
|
||||
|
||||
public class ChallengeRequest
|
||||
{
|
||||
[Required] public ClientPlatform Platform { get; set; }
|
||||
[Required] [MaxLength(256)] public string Account { get; set; } = null!;
|
||||
[Required] [MaxLength(512)] public string DeviceId { get; set; } = null!;
|
||||
[Required][MaxLength(256)] public string Account { get; set; } = null!;
|
||||
[Required][MaxLength(512)] public string DeviceId { get; set; } = null!;
|
||||
[MaxLength(1024)] public string? DeviceName { get; set; }
|
||||
public List<string> Audiences { get; set; } = new();
|
||||
public List<string> Scopes { get; set; } = new();
|
||||
}
|
||||
|
||||
[HttpPost("challenge")]
|
||||
public async Task<ActionResult<AuthChallenge>> StartChallenge([FromBody] ChallengeRequest request)
|
||||
public async Task<ActionResult<AuthChallenge>> CreateChallenge([FromBody] ChallengeRequest request)
|
||||
{
|
||||
var account = await accounts.LookupAccount(request.Account);
|
||||
if (account is null) return NotFound("Account was not found.");
|
||||
@@ -48,6 +48,10 @@ public class AuthController(
|
||||
var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||
var userAgent = HttpContext.Request.Headers.UserAgent.ToString();
|
||||
|
||||
request.DeviceName ??= userAgent;
|
||||
|
||||
var device = await auth.GetOrCreateDeviceAsync(account.Id, request.DeviceId, request.DeviceName, request.Platform);
|
||||
|
||||
// Trying to pick up challenges from the same IP address and user agent
|
||||
var existingChallenge = await db.AuthChallenges
|
||||
.Where(e => e.AccountId == account.Id)
|
||||
@@ -55,10 +59,15 @@ public class AuthController(
|
||||
.Where(e => e.UserAgent == userAgent)
|
||||
.Where(e => e.StepRemain > 0)
|
||||
.Where(e => e.ExpiredAt != null && now < e.ExpiredAt)
|
||||
.Where(e => e.Type == ChallengeType.Login)
|
||||
.Where(e => e.ClientId == device.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
if (existingChallenge is not null) return existingChallenge;
|
||||
if (existingChallenge is not null)
|
||||
{
|
||||
var existingSession = await db.AuthSessions.Where(e => e.ChallengeId == existingChallenge.Id).FirstOrDefaultAsync();
|
||||
if (existingSession is null) return existingChallenge;
|
||||
}
|
||||
|
||||
var device = await auth.GetOrCreateDeviceAsync(account.Id, request.DeviceId, request.DeviceName, request.Platform);
|
||||
var challenge = new AuthChallenge
|
||||
{
|
||||
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddHours(1)),
|
||||
@@ -295,4 +304,4 @@ public class AuthController(
|
||||
});
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -101,7 +101,6 @@ public class AuthChallenge : ModelBase
|
||||
};
|
||||
}
|
||||
|
||||
[Index(nameof(DeviceId), IsUnique = true)]
|
||||
public class AuthClient : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
@@ -117,7 +116,7 @@ public class AuthClient : ModelBase
|
||||
public class AuthClientWithChallenge : AuthClient
|
||||
{
|
||||
public List<AuthChallenge> Challenges { get; set; } = [];
|
||||
|
||||
|
||||
public static AuthClientWithChallenge FromClient(AuthClient client)
|
||||
{
|
||||
return new AuthClientWithChallenge
|
||||
|
@@ -19,8 +19,7 @@ public class OidcProviderController(
|
||||
AppDatabase db,
|
||||
OidcProviderService oidcService,
|
||||
IConfiguration configuration,
|
||||
IOptions<OidcProviderOptions> options,
|
||||
ILogger<OidcProviderController> logger
|
||||
IOptions<OidcProviderOptions> options
|
||||
)
|
||||
: ControllerBase
|
||||
{
|
||||
@@ -36,74 +35,74 @@ public class OidcProviderController(
|
||||
case "authorization_code" when request.Code == null:
|
||||
return BadRequest("Authorization code is required");
|
||||
case "authorization_code":
|
||||
{
|
||||
var client = await oidcService.FindClientByIdAsync(request.ClientId.Value);
|
||||
if (client == null ||
|
||||
!await oidcService.ValidateClientCredentialsAsync(request.ClientId.Value, request.ClientSecret))
|
||||
return BadRequest(new ErrorResponse
|
||||
{
|
||||
var client = await oidcService.FindClientByIdAsync(request.ClientId.Value);
|
||||
if (client == null ||
|
||||
!await oidcService.ValidateClientCredentialsAsync(request.ClientId.Value, request.ClientSecret))
|
||||
return BadRequest(new ErrorResponse
|
||||
{ Error = "invalid_client", ErrorDescription = "Invalid client credentials" });
|
||||
|
||||
// Generate tokens
|
||||
var tokenResponse = await oidcService.GenerateTokenResponseAsync(
|
||||
clientId: request.ClientId.Value,
|
||||
authorizationCode: request.Code!,
|
||||
redirectUri: request.RedirectUri,
|
||||
codeVerifier: request.CodeVerifier
|
||||
);
|
||||
|
||||
return Ok(tokenResponse);
|
||||
}
|
||||
case "refresh_token" when string.IsNullOrEmpty(request.RefreshToken):
|
||||
return BadRequest(new ErrorResponse
|
||||
{ Error = "invalid_request", ErrorDescription = "Refresh token is required" });
|
||||
case "refresh_token":
|
||||
{
|
||||
try
|
||||
{
|
||||
// Decode the base64 refresh token to get the session ID
|
||||
var sessionIdBytes = Convert.FromBase64String(request.RefreshToken);
|
||||
var sessionId = new Guid(sessionIdBytes);
|
||||
|
||||
// Find the session and related data
|
||||
var session = await oidcService.FindSessionByIdAsync(sessionId);
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
if (session?.AppId is null || session.ExpiredAt < now)
|
||||
{
|
||||
return BadRequest(new ErrorResponse
|
||||
{
|
||||
Error = "invalid_grant",
|
||||
ErrorDescription = "Invalid or expired refresh token"
|
||||
});
|
||||
}
|
||||
|
||||
// Get the client
|
||||
var client = await oidcService.FindClientByIdAsync(session.AppId.Value);
|
||||
if (client == null)
|
||||
{
|
||||
return BadRequest(new ErrorResponse
|
||||
{
|
||||
Error = "invalid_client",
|
||||
ErrorDescription = "Client not found"
|
||||
});
|
||||
}
|
||||
|
||||
// Generate new tokens
|
||||
// Generate tokens
|
||||
var tokenResponse = await oidcService.GenerateTokenResponseAsync(
|
||||
clientId: session.AppId!.Value,
|
||||
sessionId: session.Id
|
||||
clientId: request.ClientId.Value,
|
||||
authorizationCode: request.Code!,
|
||||
redirectUri: request.RedirectUri,
|
||||
codeVerifier: request.CodeVerifier
|
||||
);
|
||||
|
||||
return Ok(tokenResponse);
|
||||
}
|
||||
catch (FormatException)
|
||||
case "refresh_token" when string.IsNullOrEmpty(request.RefreshToken):
|
||||
return BadRequest(new ErrorResponse
|
||||
{ Error = "invalid_request", ErrorDescription = "Refresh token is required" });
|
||||
case "refresh_token":
|
||||
{
|
||||
return BadRequest(new ErrorResponse
|
||||
try
|
||||
{
|
||||
Error = "invalid_grant",
|
||||
ErrorDescription = "Invalid refresh token format"
|
||||
});
|
||||
// Decode the base64 refresh token to get the session ID
|
||||
var sessionIdBytes = Convert.FromBase64String(request.RefreshToken);
|
||||
var sessionId = new Guid(sessionIdBytes);
|
||||
|
||||
// Find the session and related data
|
||||
var session = await oidcService.FindSessionByIdAsync(sessionId);
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
if (session?.AppId is null || session.ExpiredAt < now)
|
||||
{
|
||||
return BadRequest(new ErrorResponse
|
||||
{
|
||||
Error = "invalid_grant",
|
||||
ErrorDescription = "Invalid or expired refresh token"
|
||||
});
|
||||
}
|
||||
|
||||
// Get the client
|
||||
var client = await oidcService.FindClientByIdAsync(session.AppId.Value);
|
||||
if (client == null)
|
||||
{
|
||||
return BadRequest(new ErrorResponse
|
||||
{
|
||||
Error = "invalid_client",
|
||||
ErrorDescription = "Client not found"
|
||||
});
|
||||
}
|
||||
|
||||
// Generate new tokens
|
||||
var tokenResponse = await oidcService.GenerateTokenResponseAsync(
|
||||
clientId: session.AppId!.Value,
|
||||
sessionId: session.Id
|
||||
);
|
||||
|
||||
return Ok(tokenResponse);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return BadRequest(new ErrorResponse
|
||||
{
|
||||
Error = "invalid_grant",
|
||||
ErrorDescription = "Invalid refresh token format"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
return BadRequest(new ErrorResponse { Error = "unsupported_grant_type" });
|
||||
}
|
||||
@@ -238,4 +237,4 @@ public class TokenRequest
|
||||
[JsonPropertyName("code_verifier")]
|
||||
[FromForm(Name = "code_verifier")]
|
||||
public string? CodeVerifier { get; set; }
|
||||
}
|
||||
}
|
||||
|
1826
DysonNetwork.Pass/Migrations/20250815041723_RemoveAuthClientIndex.Designer.cs
generated
Normal file
1826
DysonNetwork.Pass/Migrations/20250815041723_RemoveAuthClientIndex.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Pass.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveAuthClientIndex : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_auth_clients_device_id",
|
||||
table: "auth_clients");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_auth_clients_device_id",
|
||||
table: "auth_clients",
|
||||
column: "device_id",
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
@@ -944,10 +944,6 @@ namespace DysonNetwork.Pass.Migrations
|
||||
b.HasIndex("AccountId")
|
||||
.HasDatabaseName("ix_auth_clients_account_id");
|
||||
|
||||
b.HasIndex("DeviceId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_auth_clients_device_id");
|
||||
|
||||
b.ToTable("auth_clients", (string)null);
|
||||
});
|
||||
|
||||
|
@@ -13,171 +13,239 @@
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="FortuneTipPositiveTitle_1" xml:space="preserve">
|
||||
<value>抽卡</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_1" xml:space="preserve">
|
||||
<value>次次出金</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_2" xml:space="preserve">
|
||||
<value>游戏</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_2" xml:space="preserve">
|
||||
<value>升段如破竹</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_3" xml:space="preserve">
|
||||
<value>抽奖</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_3" xml:space="preserve">
|
||||
<value>欧气加身</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_4" xml:space="preserve">
|
||||
<value>演讲</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_4" xml:space="preserve">
|
||||
<value>妙语连珠</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_5" xml:space="preserve">
|
||||
<value>绘图</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_5" xml:space="preserve">
|
||||
<value>灵感如泉涌</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_6" xml:space="preserve">
|
||||
<value>编程</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_6" xml:space="preserve">
|
||||
<value>0 error(s), 0 warning(s)</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_7" xml:space="preserve">
|
||||
<value>购物</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_7" xml:space="preserve">
|
||||
<value>汇率低谷</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_8" xml:space="preserve">
|
||||
<value>学习</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_8" xml:space="preserve">
|
||||
<value>效率 200%</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_9" xml:space="preserve">
|
||||
<value>编曲</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_9" xml:space="preserve">
|
||||
<value>灵感爆棚</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_10" xml:space="preserve">
|
||||
<value>摄影</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_10" xml:space="preserve">
|
||||
<value>刀锐奶化</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_11" xml:space="preserve">
|
||||
<value>焊 PCB</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_11" xml:space="preserve">
|
||||
<value>上电,启动,好耶!</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_12" xml:space="preserve">
|
||||
<value>AE 启动</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_12" xml:space="preserve">
|
||||
<value>帧渲染时间 20ms</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_13" xml:space="preserve">
|
||||
<value>航拍</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_13" xml:space="preserve">
|
||||
<value>”可以起飞“</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_14" xml:space="preserve">
|
||||
<value>调色</value>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_14" xml:space="preserve">
|
||||
<value>色彩准确强如怪,拼尽全力无法战胜</value>
|
||||
<value>抽卡</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_1" xml:space="preserve">
|
||||
<value>次次出金</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_2" xml:space="preserve">
|
||||
<value>游戏</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_2" xml:space="preserve">
|
||||
<value>升段如破竹</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_3" xml:space="preserve">
|
||||
<value>抽奖</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_3" xml:space="preserve">
|
||||
<value>欧气加身</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_4" xml:space="preserve">
|
||||
<value>演讲</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_4" xml:space="preserve">
|
||||
<value>妙语连珠</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_5" xml:space="preserve">
|
||||
<value>绘图</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_5" xml:space="preserve">
|
||||
<value>灵感如泉涌</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_6" xml:space="preserve">
|
||||
<value>编程</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_6" xml:space="preserve">
|
||||
<value>0 error(s), 0 warning(s)</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_7" xml:space="preserve">
|
||||
<value>购物</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_7" xml:space="preserve">
|
||||
<value>汇率低谷</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_8" xml:space="preserve">
|
||||
<value>学习</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_8" xml:space="preserve">
|
||||
<value>效率 200%</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_9" xml:space="preserve">
|
||||
<value>编曲</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_9" xml:space="preserve">
|
||||
<value>灵感爆棚</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_10" xml:space="preserve">
|
||||
<value>摄影</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_10" xml:space="preserve">
|
||||
<value>刀锐奶化</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_11" xml:space="preserve">
|
||||
<value>焊 PCB</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_11" xml:space="preserve">
|
||||
<value>上电,启动,好耶!</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_12" xml:space="preserve">
|
||||
<value>AE 启动</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_12" xml:space="preserve">
|
||||
<value>帧渲染时间 20ms</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_13" xml:space="preserve">
|
||||
<value>航拍</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_13" xml:space="preserve">
|
||||
<value>”可以起飞“</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveTitle_14" xml:space="preserve">
|
||||
<value>调色</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_14" xml:space="preserve">
|
||||
<value>色彩准确强如怪,拼尽全力无法战胜</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_1" xml:space="preserve">
|
||||
<value>抽卡</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_1" xml:space="preserve">
|
||||
<value>吃大保底</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_2" xml:space="preserve">
|
||||
<value>游戏</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_2" xml:space="preserve">
|
||||
<value>掉分如山崩</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_3" xml:space="preserve">
|
||||
<value>抽奖</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_3" xml:space="preserve">
|
||||
<value>十连皆寂</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_4" xml:space="preserve">
|
||||
<value>演讲</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_4" xml:space="preserve">
|
||||
<value>谨言慎行</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_5" xml:space="preserve">
|
||||
<value>绘图</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_5" xml:space="preserve">
|
||||
<value>下笔如千斤</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_6" xml:space="preserve">
|
||||
<value>编程</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_6" xml:space="preserve">
|
||||
<value>114 error(s), 514 warning(s)</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_7" xml:space="preserve">
|
||||
<value>购物</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_7" xml:space="preserve">
|
||||
<value>245% 关税</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_8" xml:space="preserve">
|
||||
<value>学习</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_8" xml:space="preserve">
|
||||
<value>效率 50%</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_9" xml:space="preserve">
|
||||
<value>编曲</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_9" xml:space="preserve">
|
||||
<value>FL Studio 未响应</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_10" xml:space="preserve">
|
||||
<value>摄影</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_10" xml:space="preserve">
|
||||
<value>"No card in camera"</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_11" xml:space="preserve">
|
||||
<value>焊 PCB</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_11" xml:space="preserve">
|
||||
<value>斯~ 不烫</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_12" xml:space="preserve">
|
||||
<value>AE 启动</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_12" xml:space="preserve">
|
||||
<value>咩~</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_13" xml:space="preserve">
|
||||
<value>航拍</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_13" xml:space="preserve">
|
||||
<value>谨慎飞行(姿态模式)→ 严重低电压警报 → 遥控器信号丢失</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_14" xml:space="preserve">
|
||||
<value>调色</value>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_14" xml:space="preserve">
|
||||
<value>甲:我要五彩斑斓的黑</value>
|
||||
<value>抽卡</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_1" xml:space="preserve">
|
||||
<value>吃大保底</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_2" xml:space="preserve">
|
||||
<value>游戏</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_2" xml:space="preserve">
|
||||
<value>掉分如山崩</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_3" xml:space="preserve">
|
||||
<value>抽奖</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_3" xml:space="preserve">
|
||||
<value>十连皆寂</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_4" xml:space="preserve">
|
||||
<value>演讲</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_4" xml:space="preserve">
|
||||
<value>谨言慎行</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_5" xml:space="preserve">
|
||||
<value>绘图</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_5" xml:space="preserve">
|
||||
<value>下笔如千斤</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_6" xml:space="preserve">
|
||||
<value>编程</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_6" xml:space="preserve">
|
||||
<value>114 error(s), 514 warning(s)</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_7" xml:space="preserve">
|
||||
<value>购物</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_7" xml:space="preserve">
|
||||
<value>245% 关税</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_8" xml:space="preserve">
|
||||
<value>学习</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_8" xml:space="preserve">
|
||||
<value>效率 50%</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_9" xml:space="preserve">
|
||||
<value>编曲</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_9" xml:space="preserve">
|
||||
<value>FL Studio 未响应</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_10" xml:space="preserve">
|
||||
<value>摄影</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_10" xml:space="preserve">
|
||||
<value>"No card in camera"</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_11" xml:space="preserve">
|
||||
<value>焊 PCB</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_11" xml:space="preserve">
|
||||
<value>斯~ 不烫</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_12" xml:space="preserve">
|
||||
<value>AE 启动</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_12" xml:space="preserve">
|
||||
<value>咩~</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_13" xml:space="preserve">
|
||||
<value>航拍</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_13" xml:space="preserve">
|
||||
<value>谨慎飞行(姿态模式)→ 严重低电压警报 → 遥控器信号丢失</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_14" xml:space="preserve">
|
||||
<value>调色</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_14" xml:space="preserve">
|
||||
<value>甲:我要五彩斑斓的黑</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeTitle_15" xml:space="preserve">
|
||||
<value>洗胶片</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipPositiveContent_15" xml:space="preserve">
|
||||
<value>0 水渍</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="FortuneTipNegativeContent_15" xml:space="preserve">
|
||||
<value>“?暗盒里怎么还有!“</value>
|
||||
<comment/>
|
||||
</data>
|
||||
</root>
|
||||
|
Reference in New Issue
Block a user